Introduction
Welcome to the From Scratch Programming Class — a free class for kids and teens, and the textbook that goes with it.
This book is a record of everything we cover in class, written so you can read it on your own. Every chapter matches one class session. If you went to class, the chapter is your reference for what we did. If you missed class, the chapter is how you catch up.
How to use this book
Read it whichever way works best for you.
- Reading after class. Skim the chapter to remind yourself what we covered. The exercises you did in class are written out here so you can do them again or finish what you didn’t get to.
- Reading because you missed class. Look for the If you missed this session section near the end of the chapter. It’s a shorter, self-paced version that gets you ready for next week. Should take about twenty or thirty minutes.
- Reading ahead. Some students like to read the next chapter before class. That’s fine, even encouraged. Reading ahead doesn’t excuse you from doing the in-class exercises — that’s where the thinking happens — but coming in with questions is great.
How chapters are organized
Every chapter is built the same way:
- What we’re learning today. A short paragraph or two on the point of the session.
- You’ll need to remember from last time. A few bullet points from previous sessions you’ll lean on today.
- Part A. A new concept, with a guided exercise.
- Part B. Practice or a small project that uses Part A.
- Wrap-up. A few questions to think about.
- If you missed this session. The catch-up version.
- Stretch and extension ideas. Optional extras.
- What’s next. One sentence on where we go from here.
If class gets cut short, Part A always stands on its own. You’ll miss the practice in Part B, but you’ll have everything you need to keep going next week.
A note about asking for help
Two rules in this class:
- Ask your buddy first, then Mr. Eric. It cuts down on people waiting and makes you better at explaining what you’re stuck on, which is half of figuring it out.
- You’re done when you can explain how it works. Not when the code runs. Not when it looks right. When you can walk someone else through what your program does, line by line, you’re done. Until then, you’re not.
A note about the curriculum
This book covers about 150 hours of class — anywhere from one to two years, depending on how often your class meets. It starts with Scratch and ends with small web applications you can run in a browser. You don’t need to know anything to start. You don’t need a computer at home — most classes provide one for you to keep.
If you’re reading this years after taking the class and trying to remember how to do something, the search bar at the top of the site is your friend. The book is here for you whenever you need it.
Now turn the page. Or click the next arrow. Either way, let’s go.
For parents
From Scratch Programming Class is a free programming class for kids and teens (ages 9 and up). The full curriculum is about 150 hours of class time, which takes roughly one to two years depending on how often the class meets.
This page is what you need to know if your child is enrolled or considering it.
What your child will learn
Programming, the way real programmers actually think about it. The class moves through five big topics over the course of the curriculum:
-
Scratch (Phase 1) — visual, drag-and-drop programming. Builds the thinking habits before any typing is required. About 8-10 weeks. Kids leave with a working game they designed.
-
Python (Phases 2-4) — a real programming language used in industry. Students write, run, and debug their own programs. About 6-9 months. Kids leave able to write small useful tools.
-
Desktop apps (Phase 5) — building programs with windows, buttons, and forms — the kind of software you click on and use. About 6-8 weeks. Kids leave with their own desktop application.
-
2D games (Phase 6) — real game programming, with sprites, movement, collisions, sound, and the works. About 3-4 months. Kids leave with a game they can show off.
-
Web pages and applications (Phases 7-8) — building things that work in a browser, including small web apps with their own URLs. About 6-7 months. Kids leave with a real, functional, hosted web project.
The full design rationale lives in the curriculum decisions document in the project’s source repository. It explains why we chose each topic, in what order, and what we’re optimizing for.
What your child will also learn
Programming teaches a lot of things that aren’t programming. The ones we lean into:
- Breaking big problems into small ones. This is the most transferable skill in the curriculum. It applies to math, writing, fixing things, planning a trip — anything.
- Comfort with not knowing. Programmers face unknowns constantly. Kids who learn to think “I don’t know how to do this yet — let me figure out where to start” instead of “I can’t do this” carry that habit into everything else.
- Reading and following precise instructions. Computers are brutally literal. Working with them improves your ability to read carefully and write clearly.
- Asking for help well. The class teaches students to describe what they tried before they ask. That’s the same habit that makes a good employee, a good student, and a good collaborator.
Costs and equipment
The class itself is free. Free to enroll, free for life — students who finish the full curriculum can come back at any time, even years later, with questions or to keep working on projects.
Every student does need a computer to work on, and the class keeps that cost low and flexible. The typical options:
- Free donated laptop. When community members have donated working laptops, your family can take one at no cost. Limited availability — first come, first served.
- Refurbished mini desktop (starting around $150-$160). A small refurbished desktop computer, about the size of a hardback book (a Lenovo ThinkCentre M710q or similar). Plenty of power for everything in the curriculum at the entry price; you can spec up from there if you’d like more headroom. You’ll need a monitor, keyboard, and mouse — ones you already have at home, or ones the class can sometimes help you find through donated peripherals.
- Refurbished laptop (~$200-$300). A refurbished business- class laptop, usually a ThinkPad. Self-contained — no extra monitor or keyboard needed. Good pick if your family doesn’t have a spare monitor lying around, or if you want your child to be able to take it anywhere.
- Higher-end option. If you’d prefer a newer or more powerful computer for your child, talk to Mr. Eric directly. He can help you pick something that works for the curriculum without overspending.
- Not sure? The signup form has a “let’s discuss” option. Pick that and Mr. Eric will walk you through what fits your family.
Whichever option you pick, the machine arrives ready to use. Mr. Eric pre-installs Scratch, Python, the Python editor (Thonny), and every other tool the class uses. Your child doesn’t have to install anything to start.
Once your child has their computer, it’s theirs forever. Even if your child stops attending, the machine stays with your family.
If equipment cost is a real concern at any tier, please let Mr. Eric know. The class exists to teach kids who want to learn — families shouldn’t be excluded based on what they can afford. There’s flexibility on the equipment side, and donated equipment exists specifically for families for whom the standard tier isn’t comfortable.
Can you donate equipment?
The donated-laptop tier above only works because other families in the community give equipment. If you have a working computer, monitor, keyboard, mouse, or other peripherals you no longer use — please consider donating to the class. What you donate goes straight to other kids in the class, especially the ones whose families need the help.
What’s most useful:
- Working laptops that can run Linux Mint (basically anything from the last 8-10 years). The class re-images them and uses them as student machines.
- Monitors — any size, working condition. Paired with the refurbished mini desktops.
- USB keyboards and mice. Standard models, nothing fancy.
- Power adapters and chargers, especially if you have spares for laptop or desktop equipment.
The signup form includes a “want to donate?” question. Or you can mention it to Mr. Eric directly any time — even if your child isn’t enrolled, donated equipment is welcome and goes to a kid who needs it.
When and where
Local logistics — meeting day, meeting time, venue — vary by class. Check with your local instructor for specifics.
Sessions usually run 90 minutes to 2 hours, once a week. Some classes meet twice a week and finish the full curriculum faster.
What you can do as a parent
The class doesn’t ask anything specific of you. But if you’re inclined, the things that help most are:
- Be curious about what your child is building. Ask to see it. Let them demo. Their explanations to you reinforce their learning.
- Don’t compare them to other students. The class is mixed- age and mixed-skill on purpose. Every kid moves at their own pace.
- Encourage them to keep working at home if they want to. Personal projects are where the real learning happens. They don’t have to — but if they want to, encourage it.
- Don’t worry if they say “it’s hard.” It’s supposed to be hard. Hard is where learning happens. The class is built to give them the tools to push through hard.
Questions
For questions about your local class (logistics, scheduling, the teacher), contact the class directly.
For questions about the curriculum content (what’s being taught, how, why), the curriculum decisions document covers most of it.
For anything else, the introduction and the for-students page explain how the class and this book work.
For students
A quick guide to how the class and this book work.
Two rules
These two rules cover most of what you need to know about how class works:
- Ask your buddy first, then Mr. Eric. Every student has a buddy, assigned by Mr. Eric. When you get stuck, ask your buddy before raising your hand. If you and your buddy are both stuck, then it’s Mr. Eric’s turn.
- You’re done when you can explain how it works. An exercise isn’t finished just because the code runs. It’s finished when you can walk someone else through what your code does. If you can’t explain it, you’re not done yet.
Each chapter has two parts
In class, we usually do Part A in the first half and Part B in the second half. If class gets cut short for any reason, Part A always stands on its own — you won’t be missing anything required.
Part B usually has tiered goals:
- Base — what everyone should aim to finish.
- Stretch — try it if you have time.
- Extension — for if you want to keep going past stretch.
You’re done when you finish the base goal. Stretch and extension are bonus, never required.
You can stop when you’re done
If you’re someone who can’t stop until something is “perfect” — you have permission to stop. The base target is the actual finish line. Mr. Eric will tell you to stop sometimes. Listen to him.
If you miss a session
Find the chapter for the session you missed. Skip to the section called If you missed this session. It’s a shorter version that gets you ready for next week. About twenty or thirty minutes of work on your own.
Don’t worry about it. Missing sessions happens to everyone.
When you get stuck
Stuck is normal. Programming is mostly being stuck and then getting unstuck. The Getting unstuck appendix has a checklist that helps. Work through it in order before asking for help. Half the time you’ll fix it yourself.
Words you don’t know
When you hit a word you’ve never seen before — sprite, sequence, conditional, variable, whatever — check the Glossary. It explains words in plain language. The glossary grows as the curriculum moves forward, so words from later sessions get added when those sessions start.
Reading ahead
If you want to read the next chapter before class, go for it. Just don’t skip the in-class exercises when we get there — the exercises are where the actual learning happens.
Working on your own at home
Anything you do in class, you can also do at home. The Installing on your own computer appendix tells you how to set up Scratch (and later, Python) on any computer you have access to.
Personal projects — things you build on your own time, not for class — are the highest-value thing you can do as a programmer. If you have an idea, build it. Bring it to class and show Mr. Eric. We love seeing what you build.
A few more things to know
- You can’t break the computer by clicking buttons. Software is hard to break by accident. Click everything. Try things. That’s how you learn.
- Save your work. Especially big projects. Files have a way of vanishing when you didn’t mean them to.
- Ask “why” sometimes. When something works and you’re not sure why, ask. The “why” is usually more important than the “how.”
- Be a good buddy. You’ll often be the one helping someone else get unstuck. The best way to help is the same as the best way to get unstuck: ask them what they want to happen, and what they’ve already tried. Don’t just take their keyboard.
Where to start
If you’re brand new, start at Session 1: Welcome & the peanut butter problem. Or pick the chapter for whichever session is happening this week. The textbook is yours — use it however helps.
Phase 1 — Scratch
Welcome to the first phase of the From Scratch Programming Class. This is where you start.
What this phase is
Phase 1 teaches you the ideas of programming using a tool called Scratch. You don’t type code — you snap colorful blocks together. The blocks make a cat (and other characters) do things on a screen.
That probably sounds like a kid’s toy. It is, sort of. It’s also how millions of people learn to program for the first time, because Scratch lets you focus on the thinking part of programming without getting tangled up in typing, spelling, and weird symbols.
By the end of Phase 1, you’ll have built a complete, playable game of your own design. Then in Phase 2 we’ll switch to a real programming language (Python) — and you’ll discover that almost everything you learned in Scratch carries straight over.
What you’ll learn
The nine sessions of Phase 1 teach the core ideas every programmer uses, every day, for their entire career:
| Session | Idea | What it lets you do |
|---|---|---|
| 1 | Welcome & the peanut butter problem | Understand why precision matters in programming |
| 2 | Sequences | Tell sprites to do things, in order |
| 3 | Loops | Make sprites do something many times without writing it many times |
| 4 | Events | Control sprites in real time with the keyboard and mouse |
| 5 | Conditionals | Let your program decide what to do based on what’s happening |
| 6 | Variables | Make your program remember things, like a score |
| 7 | Putting it together | Combine everything into one small game |
| 8-9 | Milestone project | Plan, build, and demo a game of your own |
Every one of those concepts shows up again in Python (Phase 2-4), in desktop apps (Phase 5), in 2D games (Phase 6), in web pages (Phase 7), and in web applications (Phase 8). What changes is the language and the tools. The ideas are the same.
What you’ll build
By the end of Session 7, you’ll have built a small arcade-style game called Apples and Rocks: a cat that catches falling apples for points and dodges falling rocks (one rock and game over). Combines every idea from Sessions 2-6.
By the end of Session 9, you’ll have built a second game of your own design and shown it to the class. Could be a maze, a pet care game, a clicker, a story, a quiz — whatever you imagine, using everything you’ve learned.
What you’ll need
If you’re in the class at Old Zion Hill, the answer is: nothing. The class provides a computer with Scratch already installed, and you keep it.
If you’re following along on your own, you need:
- A computer (any computer — Windows, Mac, Linux, even a school Chromebook works).
- Either an internet connection (so you can use Scratch at scratch.mit.edu) or Scratch Desktop installed on your machine. The Installing on your own computer appendix has details.
How sessions work
Each session is one class meeting, about 90 to 120 minutes long. Every session has the same shape:
- Part A introduces a new idea with a guided exercise. If class gets cut short, Part A always stands on its own.
- Part B is open-ended practice or a project that uses Part A. Sometimes tiered into base / stretch / extension goals.
- Wrap-up is a few minutes of sharing what you did.
Each session also has an If you missed this session section near the end of the chapter — about 20 to 30 minutes of self-paced work that gets you ready for next week if you couldn’t make it.
Where to start
Session 1: Welcome & the peanut butter problem is where it begins. If you went to class, that’s the chapter that matches what we did. If you’re starting from this textbook on your own, that’s where to read first too.
When you’re stuck, the Getting unstuck appendix has a checklist that helps. The Glossary explains the vocabulary words we use in class.
Welcome. Let’s go.
Session 1: Welcome & the peanut butter problem
Session 1: Welcome & the peanut butter problem
Phase 1 — Scratch · Session 1 of 9
What we’re learning today
Today is your first day of programming class. We’re going to talk about what programming actually is, play a game that will probably make Mr. Eric look ridiculous, and open up the program we’ll use for the next couple of months. By the end, you’ll know why being precise matters more than being fast, and you’ll have clicked around inside Scratch enough to feel like it’s not a stranger.
You’ll need to remember from last time
Nothing! This is your first session. Welcome.
Part A: What is programming, really?
Let’s start with a question: what is a computer?
You probably said something like “a machine that does stuff,” or “a thing with a screen.” Those are true. But here’s a harder question: how does a computer do stuff? Who tells it what to do?
Here’s the answer we’re going to spend a whole year unpacking:
A computer does exactly what someone tells it to do. Nothing more, nothing less. It can’t guess what you mean. It only knows what you say.
That sounds simple. It isn’t. We’re going to prove it right now.
The peanut butter and jelly problem
Mr. Eric is going to pretend to be a computer. You are going to be the programmers. Your job: tell the computer (Mr. Eric) how to make a peanut butter and jelly sandwich. The computer will do exactly what you say.
You’ll go around the room, each person giving one instruction at a time. Mr. Eric will follow each instruction as literally as possible. If you say “put the peanut butter on the bread,” he might just set the unopened jar on the unopened bag of bread. Because that’s what you said.
Here’s what you’ll discover:
- The computer doesn’t know what “open” means unless you say how.
- The computer doesn’t know what to spread peanut butter with unless you say what.
- The computer doesn’t know that bread comes in slices, or that the slices need to be next to each other, or that the jelly goes on the bread and not on the table.
You will probably end up laughing a lot. That’s fine. The laughing is the lesson.
What this means
A computer is like Mr. Eric-as-computer, but worse. At least Mr. Eric knows what bread is. A real computer doesn’t. It doesn’t know what anything is until you tell it.
Here’s the cool part:
When you learn to give instructions that precise, you can make the computer do anything.
Games. Apps. Websites. Tools that make your life easier. Things nobody has ever made before. That’s what we’re learning to do this year.
Talk about it
Before we move on, talk through these questions as a group:
- What were some things we forgot to tell the computer that turned out to matter?
- What kinds of things did the computer assume? Was it ever right?
- If you had to write down all the steps to make a sandwich so a real computer could do it, how many steps do you think it would take?
(The real answer to that last one: way more than you’d guess. Hundreds. Maybe more.)
Part B: Meet Scratch
Now let’s meet the tool we’ll use for the next couple of months. It’s called Scratch, and it’s a programming language designed for people who are just starting out. You build programs by snapping blocks together — like LEGO, but the blocks make things happen on your screen.
Open Scratch on your computer. Look for the orange cat icon on your desktop, or find it in the Applications menu.
A tour of the screen
Scratch divides the screen into a few areas. Don’t worry about memorizing this — you’ll get used to it fast.
- The stage (top right). This is where things happen. The orange-and-white cat you see is called a sprite — a character on the stage.
- The blocks panel (left side). These are instructions you can give to sprites. They’re grouped by color. Blue blocks make things move. Purple blocks change how things look. There are lots of others. We’ll learn them as we go.
- The script area (middle). This is where you build your program by snapping blocks together.
- The sprite list (bottom right). All the sprites in your project. Right now, just the cat.
- The green flag and red stop sign (top of the stage). Green flag runs your program. Red stop sign halts it.
Try this together
Let’s get the cat to do something on purpose.
- Drag a
move 10 stepsblock (it’s blue, near the top of the blocks panel) into the script area in the middle. - Click on the block. The cat moves a little.
- Click on it again. It moves more.
- Drag a
turn 15 degreesblock onto the bottom of the first one. They should snap together. - Click on the stack. The cat moves and turns.
- Click on it twenty more times.
Your cat should now be in a weird place doing weird things. That’s correct.
Now go explore
For the rest of class, just play. Try any blocks you want. Make the cat do silly things. Try to make it walk off the screen. Try to make it spin in circles. Try to make it say something (hint: look at the purple blocks).
There are no wrong answers right now. You’re getting comfortable. That’s the whole goal.
If you find a block that looks interesting and you don’t know what it does — try it. That’s how you’ll learn most of what you learn this year.
Wrap-up
Before we leave, we’ll go around and share:
- What was the silliest thing you got the cat to do?
- Did anything happen that you didn’t expect?
- Did you find any blocks that looked interesting but you didn’t try?
You did great today. Next week, we’ll start telling the cat what to do on purpose.
If you missed this session
You missed Mr. Eric trying to spread peanut butter with a comb. There’s no replacement for that, sorry. But here’s what you need to know:
- Programming is telling a computer exactly what to do. Computers can’t guess. They do what you say, even when what you say is wrong.
- Open Scratch on your computer. It’s already installed — look for the orange cat icon on your desktop or in the Applications menu.
- Drag a few blocks from the left panel into the middle area, snap them together, and click them. Try the blue ones first (they make things move). Mess around for fifteen or twenty minutes.
When you come next week, you’ll be ready to keep up. If anything’s confusing, ask your buddy at the start of class.
Stretch and extension ideas
If you finished early or want to keep going at home:
- Find a
sayblock (purple) and make the cat say something. - Add a second sprite from the sprite library (look for the cat-with-plus icon at the bottom right).
- Try to make the cat walk in a square shape. Hint: how many turns? How many degrees each turn?
What’s next
Next week we’ll learn how to make sprites do things in order — and what happens when the order is wrong.
Session 2: Sequences and the stage
Session 2: Sequences and the stage
Phase 1 — Scratch · Session 2 of 9
What we’re learning today
Last week you played around in Scratch and got the cat to do silly things. Today we’ll do the same kinds of things, but on purpose. You’ll learn how to start a program with the green flag, how to make a sprite do several things in a row, and what happens when the order of those things is wrong. By the end you’ll have built a tiny scripted scene — your first real program.
You’ll need to remember from last time
- Computers do exactly what you say. No guessing. (The peanut butter rule.)
- Scratch is on your computer and you know how to open it.
- Blocks snap together like LEGO in the script area.
- The green flag runs your program. The red stop sign halts it.
Part A: Telling the cat what to do, in order
Open Scratch and start a new project. (File menu → New, or just restart Scratch.)
What “in order” means
Last week you clicked individual blocks and watched the cat react. That works, but it’s not really programming yet — you were the one deciding when each block ran. A real program does several things in order, all on its own, after you start it once.
The way you start a program in Scratch is the green flag. You’ve already seen it — it’s the green triangle at the top of the stage. We need a block that says “when the green flag is clicked, do this.” That block lives in the Events category (yellow blocks).
Drag this one out:
when [green flag] clicked
It looks a little different from other blocks — it has a curved top and no notch above it, because nothing snaps on top of it. It’s a starting block. Things snap below it.
Build a sequence
Snap these blocks together, in this order, under the green flag:
when [green flag] clicked
move 50 steps
wait 1 seconds
turn ↻ 90 degrees
move 50 steps
The wait 1 seconds block is in the Control category (orange).
The others you’ve already met.
Now click the green flag at the top of the stage.
The cat moves 50 steps, pauses for one second, turns 90 degrees, and moves 50 more steps. Every time. In that order. Click the green flag again and it does the same thing again from wherever it ended up.
That’s a sequence — a list of instructions that runs from top to bottom. It’s the most basic kind of program.
Now break it
Programmers learn fastest by breaking things on purpose, so let’s break this on purpose.
Drag the wait 1 seconds block to the top of the stack, right
under the green-flag block, so the order is now:
when [green flag] clicked
wait 1 seconds
move 50 steps
turn ↻ 90 degrees
move 50 steps
Click the green flag. What happened? The cat sits still for one second, then does the rest. Same blocks. Different order. Different behavior.
Try it again with the turn block at the very end, after both
moves. Now the cat moves in a straight line and only turns when
it’s done. Same blocks. Different order. Different behavior.
This is the second-most-important rule of programming, after the peanut butter one:
Order matters. The same instructions in a different order make a different program.
Reset the cat
If your cat has wandered to a weird spot or is facing a weird way, that’s fine — but it’s annoying when you’re trying to test your program. Two ways to reset:
- Click and drag the cat back to the middle of the stage with your mouse.
- Add a
go to x: 0 y: 0block (Motion, blue) at the very top of your program, right under the green flag. Now every time you click the green flag, the cat starts from the middle.
The second one is what programmers usually do: make the program reset itself. We’ll come back to this idea a lot.
Checkpoint: You should have a program that starts with a green flag, includes at least three blocks in a row, and behaves differently when you change the order of the blocks. This is the natural stop point if class is cut short — Part B builds on this but isn’t required.
Part B: A scripted scene
Now let’s build something that feels like a tiny piece of a story.
What you’re building
A short scene where the cat enters from the left side of the stage, walks to the middle, says “Hello!”, waits, and walks off to the right side.
That’s the base goal. It’s a little harder than what we did in Part A because there are more steps and you have to think about positions and timing.
How to think about it
Before you start clicking, talk through it in plain words. What does the cat need to do, in order?
- Start at the left side of the stage.
- Walk to the middle.
- Say “Hello!” for two seconds.
- Wait a moment.
- Walk off to the right.
That’s a sequence. Now you need to find the right blocks.
- Start at the left side:
go to x: -200 y: 0(Motion). The stage is about 480 wide, so x = -200 is near the left. - Walk to the middle: several
move 10 stepsblocks, or oneglide 2 secs to x: 0 y: 0block (also Motion). Theglideversion is smoother — try both and see which you like. - Say “Hello!” for two seconds:
say [Hello!] for 2 seconds(Looks, purple). - Wait a moment:
wait 1 seconds(Control). - Walk off to the right:
glide 2 secs to x: 240 y: 0.
Snap them all under your when green flag clicked block. Click the
flag. Watch your scene.
Try the stretch goal
If your base scene works and you have time:
- Stretch: Add a second sprite — a person, a dog, anything from
the sprite library. Have them say something back to the cat
before the cat walks off. (Tricky part: you’ll need to give the
second sprite its own
when green flag clickedblock, because every sprite has its own scripts.)
Or the extension
If you finish stretch and still have time:
- Extension: Make the cat do a short dance in the middle of the scene. A dance is just a sequence — turn a little, move a little, turn back, move back, in some pattern that looks like dancing.
Don’t worry about making it perfect. “Looks like dancing” is the goal, not “actual dance choreography.”
Wrap-up
Before we leave, let’s share:
- What did your scene do?
- Did anything happen that you didn’t expect?
- Did you have to change the order of blocks to get something to work?
Done for today doesn’t mean done with the topic. Sequences will come back every week from here on out. You’ll get plenty more practice.
If you missed this session
Open Scratch on your computer. Start a new project. Then:
- Drag a
when green flag clickedblock (yellow, Events) into the script area. - Snap a
move 50 stepsblock underneath it. - Snap a
wait 1 secondsblock underneath that. - Snap a
turn 90 degreesblock underneath that. - Snap one more
move 50 stepsblock underneath. - Click the green flag. Watch the cat.
- Now drag the
waitblock to a different position in the stack. Click the green flag. Watch what changes.
Then try the scripted scene from Part B above. The base goal — cat enters, walks to middle, says hello, walks off — is the catch-up exercise. About 20 minutes of work.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Use
glideblocks instead ofmoveblocks for smoother motion. - Add a backdrop (look at the bottom-right corner, “Choose a Backdrop” button). Set the scene somewhere — a forest, a city, outer space.
- Make the cat change costume at some point in the scene. Sprites often have multiple costumes built in. Look at the Costumes tab at the top of the Scratch window.
What’s next
Next week we’ll learn about loops — how to make the cat do something twenty times without dragging twenty blocks.
Session 3: Loops — doing things over and over
Session 3: Loops — doing things over and over
Phase 1 — Scratch · Session 3 of 9
What we’re learning today
Today you’ll learn one of the most important ideas in all of programming: how to tell the computer to do something many times, without writing the same instruction over and over. This is called a loop. By the end of class, you’ll have used a loop to make your cat draw a perfect square — and then you’ll see how a tiny change to the loop turns the square into a triangle, a hexagon, or a flower.
You’ll need to remember from last time
- Sequences — code runs in order, top to bottom.
- The green flag starts your program.
- The blocks
move,turn,wait, andgo to x: y:. - Order matters.
Part A: Why loops exist
Open Scratch and start a new project. Add a when green flag clicked block, like always.
Now imagine you wanted to make the cat march in a straight line —
take ten little steps, one after another. With what you know so far,
you’d have to drag ten move 10 steps blocks and snap them all
together.
Try it. Drag ten of them. It’s tedious. Ten is annoying. A hundred would be ridiculous. A thousand would be impossible to build by hand.
Programmers ran into this problem about three seconds after they invented programming. The solution they came up with is the loop: a way to say “do this a bunch of times” without writing it out a bunch of times.
The repeat block
Delete the ten move blocks (right-click → delete, or drag them off
to the left).
Now go to the Control category (orange blocks). Find this one:
repeat [10]
<empty>
It’s shaped differently from the blocks you’ve used so far — it has a mouth in the middle. Blocks go inside the mouth, and they’ll run as many times as the number you put in the top.
Snap a move 10 steps block inside the mouth, and snap the whole
repeat block under your when green flag clicked block. You should
end up with this:
when [green flag] clicked
repeat [10]
move 10 steps
Click the green flag. The cat takes ten steps in quick succession.
That’s a loop. The move 10 steps block ran ten times.
Try changing the number
Click the 10 in the repeat block and change it to 100. Click
the green flag. The cat zooms across the stage.
Change it to 3. Three little steps.
Change it to 1. Just one step. (A loop that runs once is silly,
but the computer will obey.)
Change it to 0. The cat doesn’t move. The loop runs zero times,
which is allowed.
The number controls how many times the inside runs. That’s the whole concept.
Multiple blocks inside a loop
You can put more than one block inside the mouth. Try this:
when [green flag] clicked
repeat [4]
move 50 steps
turn ↻ 90 degrees
Click the green flag.
Watch carefully. The cat moves 50 steps, turns 90 degrees, moves 50 steps, turns 90 degrees… four times.
What shape did the cat just walk?
A square.
You just told the computer to draw a square in three lines of code. Without the loop, you would have written the same two blocks four times — eight blocks instead of three. With a loop that ran 100 times, that would be 200 blocks instead of 3.
This is why loops are one of the most important ideas in programming.
Checkpoint: Every student should have built a repeat loop
with at least one block inside it and clicked the green flag to see
it run. This is the natural stop point if class is cut short.
Part B: Drawing with loops
Now we’ll do something more visually satisfying than walking the cat in invisible squares. We’ll have the cat actually draw the square as it walks.
Adding the Pen extension
Scratch has extra block categories called extensions that aren’t shown by default. We need one called Pen to draw.
Look at the bottom-left corner of the Scratch window. There’s a blue
square with a + icon — the “Add Extension” button. Click it.
A list of extensions appears. Find the one called Pen and click it.
A new category called Pen (green) now appears in your blocks
panel. You’ll see blocks like pen down, pen up, set pen color,
erase all, and a few others.
The way pen works: when pen down is active, the sprite leaves a
trail behind it as it moves. When pen up, no trail.
Draw a square
Modify your loop program so it looks like this:
when [green flag] clicked
erase all
go to x: 0 y: 0
pen down
repeat [4]
move 100 steps
turn ↻ 90 degrees
pen up
Three new blocks at the top — erase all (so old drawings don’t
stick around), go to x: 0 y: 0 (so the cat starts in the middle),
and pen down (so the trail starts). And one new block at the end —
pen up (so we don’t keep drawing if we add more code later).
Click the green flag. Watch the cat draw a square.
That, right there, is the moment programming starts to feel like a superpower.
Try the stretch
The base goal is the square above. If that works:
- Stretch: Change the
repeatto3and theturnto120 degrees. What shape do you get? Now try5and72. Now try6and60. There’s a pattern. Can you figure it out?- Hint: Look at the two numbers and how they relate. (If you want a really sneaky hint: 4 × 90 = 360. 3 × 120 = 360. 5 × 72 = 360. The total turning is always 360 degrees, because that’s a full trip around.)
Or the extension
If you finished stretch and want more:
- Extension: Use a nested loop — a loop inside a loop. The
inner loop draws a shape; the outer loop turns a little and draws
it again. Something like this:
Click the green flag. You should get a beautiful flower-of-squares pattern.repeat [12] repeat [4] move 50 steps turn ↻ 90 degrees turn ↻ 30 degrees
Wrap-up
Before we leave, share with the room:
- What’s the coolest shape you got the cat to draw?
- Did you try changing numbers and discover something unexpected?
- If you wanted to draw a 100-sided shape, what would the two numbers in your loop be? (You don’t have to actually try it unless you want to. But predict.)
You’ve now used your first programming concept that isn’t in the peanut butter rule. Loops are the first big idea where the computer does something you didn’t tell it to do explicitly — it does the inside part many times because you set it up to. That’s a real shift.
If you missed this session
Open Scratch and start a new project. Then:
- Add a
when green flag clickedblock. - From the Control category, drag out a
repeat 10block. Snap amove 10 stepsblock inside its mouth. Snap the whole thing under the green flag. Click the green flag. The cat moves ten times in a row. - Change the inside to
move 50 stepsandturn 90 degrees, and change the repeat number to4. Click the green flag. The cat walks a square. - Click the Add Extension button (bottom-left). Pick Pen.
- Add
erase all,go to x: 0 y: 0, andpen downto the top of your program (in that order). Click the green flag. The cat draws a square.
Then try Part B’s stretch goal: change the numbers to draw a triangle, a pentagon, a hexagon. About 25 minutes of work.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Use
set pen color to(Pen category) to change the color of your drawing. - Use
set pen size toto make the lines thicker. - Make a spiral by combining
moveandturnoutside a loop:
Each time around the loop, the cat takes a bigger and bigger swing. Actually no — wait, did it? Look carefully. The loop body is the same every iteration. So why does it look like a spiral and not a circle?repeat [50] move 5 steps turn ↻ 10 degrees - Try using a
foreverblock instead ofrepeat. (Hint: you’ll need the red stop sign to make it stop.)
What’s next
Next week we’ll learn about events — how to make sprites react to keyboard presses, mouse clicks, and other things that happen while a program is running, not just when it starts.
Session 4: Events — making things happen when...
Session 4: Events — making things happen when…
Phase 1 — Scratch · Session 4 of 9
What we’re learning today
Up until now, all your programs started one way: you clicked the green flag. Today we’ll learn how to make sprites react to other things — a key being pressed, a sprite being clicked, all kinds of moments while your program is running. These are called events, and they’re how you go from “the program does its thing” to “I’m controlling the program in real time.”
You’ll need to remember from last time
- Sequences — code runs in order, top to bottom.
- The
repeatloop — does the inside N times. - Each sprite has its own scripts. When you switch sprites in the bottom-right panel, you see that sprite’s scripts.
- The green flag is one kind of starter block. There are others.
Part A: More than one way to start
Open Scratch and start a new project.
So far, every program you’ve written starts with the same block:
when [green flag] clicked
That block is in a category called Events (yellow). It’s not the only block in there. Take a look at the Events category. You’ll see several other “when X happens” blocks. They all work the same way: when the thing happens, the script underneath runs.
The two we’ll use today:
when [space] key pressed— runs when you press the space key. (You can change which key it watches for by clicking the dropdown.)when this sprite clicked— runs when you click on the sprite with your mouse.
A sprite can have as many scripts as you want. Each one has its own starter block, and each one runs independently when its event fires.
Build your first multi-event sprite
Try this. Snap together three separate scripts on the cat:
when [green flag] clicked
go to x: 0 y: 0
when [space] key pressed
say [Hi!] for 1 seconds
when this sprite clicked
turn ↻ 30 degrees
These are three separate stacks of blocks, not one big stack. Don’t try to snap them together. They each have their own starter and they live separately in your script area.
Now click the green flag. The cat goes to the middle.
Press the spacebar. The cat says “Hi!”
Click on the cat. It turns 30 degrees.
That’s three different events triggering three different behaviors, all on one sprite.
Cat that walks with the arrow keys
Now let’s make the cat respond to the arrow keys. We want:
- Right arrow: cat moves right
- Left arrow: cat moves left
- Up arrow: cat moves up
- Down arrow: cat moves down
That’s four scripts. Each one is short:
when [right arrow] key pressed
change x by 10
when [left arrow] key pressed
change x by -10
when [up arrow] key pressed
change y by 10
when [down arrow] key pressed
change y by -10
change x by 10 (Motion category) shifts the sprite right by 10
units. change x by -10 shifts it left. Same idea for y, except y
is up/down.
Build all four scripts. Then press the arrow keys.
Your cat now walks around the stage, controlled by you. You’re not clicking blocks anymore — you’re playing a tiny game with a cat you just programmed.
If the movement feels a little choppy when you hold an arrow key down — that’s because the keyboard takes a moment before it starts “repeating” the keypress. We’ll fix this in a later session. For now, quick taps work fine.
Checkpoint: You have a sprite with at least three separate scripts triggered by three different events (one of which is a keyboard event), and they all work. This is the natural stop point if class is cut short.
Part B: Two players, one stage
Now let’s add a second sprite that responds to its own keys.
Add a second sprite
Look at the bottom-right corner of the Scratch window. Click the Choose a Sprite button (the cat-with-plus icon). Pick anything — a dog, a person, a robot. We’ll use it as a second player.
When you add the new sprite, the scripts area in the middle of the screen switches to that sprite’s scripts — which are empty right now. Notice the cat’s scripts are still there; you just don’t see them while you’re looking at the dog. To switch back, click the cat in the bottom-right panel.
Give your second sprite WASD movement
While looking at the second sprite, build four scripts that mirror the cat’s, but using W, A, S, D instead of arrow keys:
when [w] key pressed→change y by 10when [a] key pressed→change x by -10when [s] key pressed→change y by -10when [d] key pressed→change x by 10
Click the green flag (or just start pressing keys).
Now you have two sprites controlled independently by two keyboards-worth of keys. Try it with a buddy: one of you uses arrow keys, the other uses WASD. You’re both on the same stage at the same time.
This is the base goal. If you got that working, you’re done for today.
Stretch goal
Make each sprite do something interesting when clicked. The cat might say something. The other sprite might change costume (look at the Costumes tab at the top of the Scratch window — many sprites come with multiple costumes).
Extension
Build a third sprite — a “ball” or “object” — that doesn’t move on
its own but does something when clicked. For example: when clicked,
it teleports to a random spot using go to x: <pick random> y: <pick random> (you’ll find pick random in the Operators category, the
green blocks).
Now your two players can race to click the ball after it teleports. A tiny game.
Wrap-up
Before we leave, share with the room:
- What’s something cool your sprites do that you didn’t expect?
- If you and your buddy played the two-player WASD/arrows version, what was harder than you thought it would be?
- If you could add one more event to your sprite, what would it do?
You learned something important today that’s not just about Scratch: a program can do many things in response to many different triggers, all at once. Real apps work this way. When you click something on a website, that’s an event. When you press a key in a game, that’s an event. You now know what’s happening underneath.
If you missed this session
Open Scratch and start a new project. Then:
- Look at the Events category (yellow blocks). Drag out a
when [space] key pressedblock. - Snap a
say [Hi!] for 1 secondsblock under it. Press the spacebar to test. - Drag out a
when this sprite clickedblock. Snap aturn 15 degreesblock under it. Click the cat to test. - Now build the four arrow-key movement scripts described in Part A above (right, left, up, down). About 10 minutes.
- Then try Part B — add a second sprite, give it WASD movement.
When you come next week, you’ll be ready. If anything’s confusing, ask your buddy at the start of class.
Stretch and extension ideas
- Use
when [any] key pressedto make a sprite react to any key. Useful for “press any key to start” effects. - Use
start sound(Sound category) to play a sound when an event fires. Several sprites come with sound effects built in. - Add a backdrop to your scene (bottom-right “Choose a Backdrop” button). Doesn’t change behavior, but makes everything look like it belongs somewhere.
What’s next
Next week we’ll learn about conditionals — how to make your sprites decide what to do based on what’s happening, like “bounce if you hit the wall” or “say ‘caught!’ if you touch the apple.”
Session 5: Conditionals — making decisions
Session 5: Conditionals — making decisions
Phase 1 — Scratch · Session 5 of 9
What we’re learning today
So far your programs have always done the same thing every time they ran. Today we’ll learn how to make a program check something about the world — “is the cat touching the apple?” — and decide what to do based on the answer. This is called a conditional, and combined with everything you already know, it’s enough to build your first real game.
You’ll need to remember from last time
- Events —
when [key] pressed,when this sprite clicked. - Each sprite has its own scripts.
- Sequences run top to bottom; loops repeat.
- The arrow-key cat you built last week (or its equivalent in the catch-up exercise).
Part A: “If something, then do this”
Open Scratch and start a new project.
A conditional is the programming idea of “check first, then decide.” In English you use them all the time: “if it’s raining, take an umbrella.” The “check” is is it raining? The “decision” is take an umbrella, or don’t.
Scratch has a block for this. It’s in the Control category (orange):
if < > then
<empty>
Like the repeat block, it has a mouth. Things go inside the
mouth, and they only run if the condition (the thing in the
diamond at the top) is true.
The diamond shape at the top of the if block is a slot for a
condition — a question Scratch can answer with yes or no. To
fill the slot, you need a block that’s also shaped like a diamond.
Take a look at the Sensing category (light blue). Notice that some blocks there are diamond-shaped:
<key [space] pressed?>— answers “is the space key being pressed right now?”<touching [Sprite1]?>— answers “is this sprite touching that other sprite?”<touching color []?>— answers “is this sprite touching that color?”<mouse down?>— answers “is the mouse button being held?”
Each of those answers yes-or-no. You drop one into the diamond slot
of an if block, and the inside runs only when the answer is yes.
Smooth movement (a callback to last week)
Remember how your cat felt a little choppy when you held down the arrow key? Conditionals fix that.
Last week you built four scripts, one per arrow key. This week we’ll build one script, with conditionals inside a forever loop.
Add a cat to your project (it should be there by default). Build this:
when [green flag] clicked
forever
if <key [right arrow] pressed?> then
change x by 5
if <key [left arrow] pressed?> then
change x by -5
if <key [up arrow] pressed?> then
change y by 5
if <key [down arrow] pressed?> then
change y by -5
Four if blocks, all stacked inside a forever loop, all under
one green flag.
The forever block (Control, orange) is the loop equivalent of
“keep doing this until I stop you.” Inside it, every fraction of a
second, Scratch checks all four if blocks. If an arrow key is
being held down, the corresponding change block runs — that
fraction of a second, every fraction of a second, while the key is
held.
Click the green flag. Hold an arrow key. The cat moves smoothly, because every fraction of a second the program is asking “is this key pressed?” and acting on the answer.
That’s the same arrow-key movement you built last week, but now it feels right. That’s the power of conditionals plus loops.
Checkpoint: Your cat moves smoothly with the arrow keys when
you hold them down, using a single forever loop with four if
blocks inside. This is the natural stop point if class is cut
short.
Part B: Catch the apple
Now we’ll build something that actually feels like a game.
What you’re building
An apple sprite falls from the top of the stage. Your cat moves along the bottom (using your smooth arrow-key movement from Part A). When the cat catches the apple, the apple says “Yum!” and goes back to the top to fall again.
It’s a tiny game. You’ll know if you can catch the apple or not. That’s the whole point.
Set up the stage
You already have your cat with smooth arrow-key movement. Two small adjustments:
- Move the cat to the bottom of the stage. Add
go to x: 0 y: -130inside thewhen green flag clickedscript (above the forever loop). Now the cat starts near the bottom every time. - For this game, the cat only needs to move left and right. You can
delete the
up arrowanddown arrowifblocks if you want, or leave them — your choice.
Add the apple
Click the Choose a Sprite button (bottom right). In the search box, type “apple.” Pick any apple-looking sprite. (Don’t see one? Any small sprite works. Pick a fruit, a ball, anything that can “fall.”)
Make sure you’re now editing the apple’s scripts (click the apple in the bottom-right panel; the script area should be empty).
Build this on the apple:
when [green flag] clicked
go to x: pick random -200 to 200 y: 180
forever
change y by -3
if <touching [Sprite1] ?> then
say [Yum!] for 1 seconds
go to x: pick random -200 to 200 y: 180
A few things to notice in this script:
pick random -200 to 200is in the Operators category (the green blocks). It’s another diamond-shaped block, but it goes into thexslot ofgo tobecause that slot accepts numbers. It picks a random x-coordinate each time, so the apple appears somewhere different.Sprite1is the cat’s name. Scratch may have renamed it; if your cat is called something else (like “Cat”), pick that name from the dropdown.- The script flow: the apple starts at a random spot at the top (y = 180 is near the top), then forever it falls (changes y by -3), and on every fall it checks “am I touching the cat?” If yes, it says “Yum!” and resets to the top.
Click the green flag. The apple falls. Move the cat with the arrow keys. Catch it.
You just built a game.
That’s the base goal. If you got the apple to fall, the cat to catch it, and the apple to reset — you’re done for today.
Stretch goal
What happens when the apple reaches the bottom of the stage and the cat didn’t catch it? Right now: it just keeps falling forever and disappears off screen. That’s not great game design.
Add a “missed!” condition. Inside the forever loop, after the
existing if, add another:
if <(y position) < -180> then
say [Missed!] for 1 seconds
go to x: pick random -200 to 200 y: 180
y position is in the Sensing category (it’s not a diamond — it’s a
rounded block, because it returns a number, not a yes/no answer).
The <> comparison is in the Operators category — look for < in
the green blocks.
Now if the apple falls past the bottom of the stage (y < -180), it says “Missed!” and resets to the top. The game has a real “miss” condition.
Extension
Add a second falling object. A different fruit, a coin, anything. Make it fall faster (or slower) than the apple. The cat now has to catch both, possibly making choices about which to chase.
To add it: the new sprite is just another sprite. Build the same
“forever fall + if touching cat then yum + reset” script on it. The
only thing you change is the speed (change y by -5 instead of
-3, for instance).
Two falling sprites at different speeds is twice as hard. Your buddy can play it.
Wrap-up
Before we leave, share with the room:
- Did anyone catch the apple ten times in a row? Five?
- What was the trickiest part of getting the apple to reset correctly?
- For the kids who did the stretch — what made you decide what number to use for “the bottom of the stage”?
You did something bigger today than just learning conditionals. You
combined events (arrow keys), loops (forever), and
conditionals (if touching) to build something that looks and
feels like a real game. Every game you’ll ever play uses these three
ideas, just with more sprites and more conditions.
You’re not even halfway through Phase 1, and you already have everything you need to build games.
If you missed this session
Open Scratch and start a new project. Then:
- Build the smooth arrow-key cat from Part A (one
foreverloop, fourifblocks). About 10 minutes. - Add an apple sprite (or any sprite that can “fall”).
- Build the apple’s falling-and-resetting script described in Part B above. About 15 minutes.
- Click the green flag. Move the cat. Try to catch the apple.
When you come next week, you’ll be ready. If anything’s confusing, ask your buddy at the start of class.
Stretch and extension ideas
- Use
if/else(Control category — likeif, but with two mouths). Lets you do “if X then this, otherwise that” in one block. More elegant than twoifblocks for the same situation. - Make the apple change costume when caught, then change back when
it resets. (Look at the apple’s Costumes tab; try
next costume.) - Use
start sound(Sound category) to play a sound when the cat catches the apple. - Add a
wait 0.05 secondsblock at the end of the apple’s forever loop. What changes? Why?
What’s next
Next week we’ll learn about variables — how to make Scratch remember things, like a score. That’ll let you finally turn your catch-the-apple game into a real game with points.
Session 6: Variables — remembering things
Session 6: Variables — remembering things
Phase 1 — Scratch · Session 6 of 9
What we’re learning today
Today you’ll learn how to make Scratch remember a number — like a score, a count, or a level. Once you can do that, you can finally turn last week’s catch-the-apple game into a real game with a score on the screen that goes up every time you catch one. The thing that makes Scratch remember is called a variable, and it’s one of the ideas you’ll use in every program you ever write.
You’ll need to remember from last time
- Conditionals (
ifblocks) and the forever loop. - Sensing blocks like
<touching ...?>. - The catch-the-apple game you built last week (or its catch-up version).
Part A: A box with a number in it
Open Scratch and start a new project. Keep the cat.
A variable is the programming word for “a labeled box that holds something.” You give the box a name, you put a number in it, and you can change the number whenever you want. The computer remembers what’s in the box for as long as your program is running.
Real-world example: the score in a basketball game is a variable. It starts at 0. Every time a team scores, the number goes up. The scoreboard remembers it. That’s all a variable is.
Make your first variable
Look at the Variables category in the blocks panel (orange — but a different orange from Control). At the top of it, there’s a button called Make a Variable. Click it.
Scratch asks for a name. Type clicks. Leave the rest at the
defaults (For all sprites). Click OK.
Several things just happened:
- A new set of blocks appeared in the Variables category, all
related to your
clicksvariable. - A small display labeled “clicks” appeared in the top-left corner
of the stage. That’s your variable’s value, shown in real time.
Right now it shows
0.
Use the variable
Build this on the cat:
when [green flag] clicked
set [clicks] to 0
when this sprite clicked
change [clicks] by 1
Two scripts. One resets the variable to 0 when you click the green flag. The other adds 1 to the variable every time you click the cat.
Click the green flag. The display shows 0.
Click the cat. The display shows 1. Click again. 2. Again. 3.
You just made the computer remember something. Every click adds one. The variable holds the running total.
Click the green flag again. Back to 0. Click the cat. 1 again.
The variable resets when you start over, then counts up from there.
Show the variable to the cat
You can also use the variable’s value inside other blocks. Add a third script:
when [space] key pressed
say (clicks) for 1 seconds
The (clicks) block (rounded, in Variables — it’s the rounded one
right next to your variable name) gives you the current value.
Click the green flag. Click the cat a few times. Press space. The cat says the current value out loud. Click the cat more. Press space again. The cat says the new value.
That’s the three things you do with a variable: set it, change it, and read it.
Checkpoint: You have a variable that increases when you click the cat, resets when you click the green flag, and is displayed by the cat when you press space. This is the natural stop point if class is cut short.
Part B: Score in your catch-the-apple game
Now we’ll add a score to the game from last week.
Open last week’s project
Open the catch-the-apple project from Session 5. (If you don’t have it — for example because you missed last week — go back to Session 5 and build the base game first, or use the catch-up version.)
You should have a cat that moves with the arrow keys (smooth
movement, with a forever loop and four if blocks) and an apple
that falls from the top, says “Yum!” when the cat catches it, and
resets to a random spot at the top.
Add a score variable
Click Make a Variable in the Variables category. Name it
score. Click OK. The score display appears in the top-left of the
stage.
Reset the score on green flag
The cat already has a when green flag clicked script. Add to it:
when [green flag] clicked
go to x: 0 y: -130
set [score] to 0
forever
if <key [right arrow] pressed?> then
change x by 5
if <key [left arrow] pressed?> then
change x by -5
(Just add set [score] to 0 somewhere before the forever loop.)
Make the apple update the score
Click the apple sprite in the bottom-right panel to switch to
its scripts. Find the part where it says “Yum!” and resets to the
top. Add change [score] by 1 to that block:
when [green flag] clicked
go to x: pick random -200 to 200 y: 180
forever
change y by -3
if <touching [Sprite1] ?> then
change [score] by 1
say [Yum!] for 1 seconds
go to x: pick random -200 to 200 y: 180
Click the green flag. Catch the apple a few times.
The score in the corner goes up every time. You’re keeping score in your own game.
That’s the base goal.
Stretch goal
Add a “missed” penalty. If the apple falls past the bottom of the stage without being caught, decrease the score by 1.
Find the if <(y position) < -180> block from last week’s stretch
goal (or add it now). Inside it, add change [score] by -1. So:
if <(y position) < -180> then
change [score] by -1
say [Missed!] for 1 seconds
go to x: pick random -200 to 200 y: 180
Now your score reflects skill — catches add, misses subtract. Carelessness costs you points.
Extension
Add a second variable called best. Whenever score goes higher
than best, update best. The display now shows two numbers: your
current score, and the best score you’ve gotten this session.
Add this to a forever loop somewhere (maybe on the cat):
forever
if <(score) > (best)> then
set [best] to (score)
The <> comparison is in Operators (green). The (score) and
(best) rounded blocks are in Variables.
Now you have a “personal best” tracker. It resets when you click the green flag, but within a single play session, it keeps the high score visible.
Wrap-up
Before we leave, share with the room:
- What’s the highest score you got? Did anyone beat it after another few tries?
- For the kids who did the missed-penalty stretch — does it feel like a different game when there’s a real consequence for missing?
- What’s another game where the score would matter?
You learned the simplest version of one of the biggest ideas in programming: state. Programs that remember something — anything — between one moment and the next are running on state. Every website that knows you’re logged in, every game that tracks your progress, every app that holds a counter — all of it is variables, behind the scenes.
You also closed the loop on something. Your game from last week was missing one important piece, and now it has it. Real software gets built that way: ship something, play with it, find what’s missing, add it, repeat.
If you missed this session
Open Scratch and start a new project. Then:
- Click Make a Variable in the Variables category. Name it
clicks. Click OK. - Build the two scripts from Part A: green-flag-resets-to-0, and sprite-clicked-changes-by-1. Click the cat to test.
- Open your catch-the-apple project from Session 5 (or build the catch-up version of Session 5 first if you don’t have one).
- Add a
scorevariable. Reset it to 0 on green flag. Have the apple’s “if touching cat” block alsochange score by 1. Click the green flag and play. Score goes up each catch.
About 25 minutes of work total.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Show the variable in a “large” view — right-click on the variable display on the stage and pick “large readout.” Looks more like a real scoreboard.
- Make a “level” variable that increases every time the score hits a multiple of 10. Display both score and level.
- Use
hide variable [name](Variables category) to hide a variable from the stage when you don’t want players to see it. Useful for variables you’re tracking internally but don’t want shown. - Tinker with the
setvschangedistinction. Tryset score to 10and watch the score jump. The difference between “put this exact number in the box” and “add this number to whatever’s in the box” is worth feeling.
What’s next
Next week we’ll put everything together — sequences, loops, events, conditionals, sensing, variables — and build a single small game that uses all of it. After that, you’ll plan and build your own milestone project.
Session 7: Putting it together — a small game
Session 7: Putting it together — a small game
Phase 1 — Scratch · Session 7 of 9
What we’re learning today
You’ve now learned every basic idea you need to build games: sequences, loops, events, conditionals, sensing, and variables. Today we’ll combine all of them into one small game called Apples and Rocks — catch the falling apples for points, but dodge the falling rocks or it’s game over. By the end, you’ll have a complete, playable game with a score and a real losing condition.
You’ll need to remember from last time
- Variables —
set,change,(read), displayed on stage. - The
ifblock + sensing for collision detection. - Forever loops for things that keep happening.
- Events for arrow-key control.
- Multiple sprites with their own scripts.
That’s basically the whole curriculum so far. Today is the test: can you put it all together?
Part A: Build the basic game (apples falling, score going up)
Open Scratch and start a new project. (Don’t open last week’s — we’ll build the whole game from scratch so you remember every piece.)
This first half is essentially the catch-the-apple game from Sessions 5 and 6, rebuilt cleanly in one go. If you’ve kept up, this should feel like review. That’s the point — you’re proving to yourself that you can build this from memory.
Anatomy of this game (how the parts fit together)
Before you start clicking blocks, look at the shape of what you’re building. Three sprites, each with its own job:
Cat sprite (the player)
on green flag:
[reset position, reset score]
forever:
[if right arrow → move right]
[if left arrow → move left]
Apple sprite (the good thing)
on green flag:
[start at top, random x]
forever:
[fall down]
[if touching cat → +1 score, restart at top]
[if off bottom → restart at top]
Rock sprite (the bad thing) — added in Part B
on green flag:
[start at top, random x]
forever:
[fall down]
[if touching cat → say "Game Over!", stop everything]
[if off bottom → restart at top]
Each sprite is a tiny machine that runs forever once you press the green flag. They don’t know about each other except through touching checks. Keep this map in your head as you build.
Set up the cat
Build this on the cat:
when [green flag] clicked
go to x: 0 y: -130
set [score] to 0
forever
if <key [right arrow] pressed?> then
change x by 7
if <key [left arrow] pressed?> then
change x by -7
(Make a score variable first if you haven’t — Variables category
→ Make a Variable → name it score → OK.)
The cat sits at the bottom of the stage and slides left/right with the arrow keys. Score resets to 0 on green flag.
Add the apple
Click Choose a Sprite (bottom right). Search for “apple” and add one (or any small fruit/food sprite).
Switch to the apple’s scripts (click the apple in the sprite list). Build:
when [green flag] clicked
go to x: pick random -200 to 200 y: 180
forever
change y by -3
if <touching [Sprite1] ?> then
change [score] by 1
go to x: pick random -200 to 200 y: 180
if <(y position) < -180> then
go to x: pick random -200 to 200 y: 180
(Replace Sprite1 with whatever your cat is named in the dropdown.
Often it’s Cat or Sprite1.)
The apple falls, gives you a point if the cat catches it, resets to the top either way (catch or miss).
Click the green flag. Move the cat. Catch some apples. Watch the score go up.
This is the base game — same as before, just rebuilt cleanly.
Checkpoint: You have a working catch-the-apple game with a visible score that increases when the cat catches apples. This is the natural stop point if class is cut short — Part B adds the hazard mechanic but isn’t required.
Part B: Add the rock (game over!)
Now we’ll make this a real game by adding risk. Right now you can just stand still and you don’t lose anything. Boring. Time to add a falling rock that ends the game if you touch it.
Add the rock sprite
Click Choose a Sprite again. Search for “rock” — there’s a Rocks sprite in the library. (Or pick anything that looks like something you wouldn’t want to touch — a bug, a skull, a “Block.”)
Switch to the rock’s scripts. Build:
when [green flag] clicked
go to x: pick random -200 to 200 y: 180
forever
change y by -3
if <touching [Sprite1] ?> then
say [Game Over!] for 2 seconds
stop [all]
if <(y position) < -180> then
go to x: pick random -200 to 200 y: 180
The rock falls just like the apple, but:
- If it touches the cat: say “Game Over!” for 2 seconds, then
stop [all](Control category) — this freezes the entire program. - If it falls off the bottom: reset to top with new random x. (No penalty for letting a rock pass — that’s actually what you want to happen.)
Click the green flag. Move the cat. Catch apples. Dodge the rock.
When the rock catches you, the cat says “Game Over!” and everything freezes. Your final score sits on the screen as your record.
You just built a game with a real losing condition. Welcome to actual game design.
That’s the base goal for today. If your apples-and-rocks game runs and ends correctly, you’re done.
Stretch goal
Right now the apple and the rock fall at the same speed. Make the rock slightly faster than the apple to ramp up the challenge:
In the rock’s forever loop, change change y by -3 to change y by -4 or -5. Now the rock is harder to dodge.
Then make a second apple sprite (right-click the apple in the sprite list → duplicate). Two apples falling at once means more points but also more places to be at the wrong moment.
Extension
Add a “best” score variable that survives across plays in the same session. Add to the cat’s scripts:
when [space] key pressed
if <(score) > (best)> then
set [best] to (score)
(You’ll need to make a best variable first.)
Now: when you press space at the end of a game (after game over),
your score gets compared to best, and best updates if you beat
it. Across multiple games, you can keep trying to beat your own
record. Press the green flag to start over each time.
For an even bigger challenge: what if score went DOWN by 1 each
time the apple falls without being caught? Add change [score] by -1 inside the apple’s (y position) < -180 block. Now you can
have a negative score, and missing apples is genuinely costly.
Wrap-up
Before we leave, a few things to share:
- What was the highest score anyone got before the rock caught them?
- Did the rock-faster-than-apple stretch make the game feel meaningfully different?
- For the kids who did the “best” extension — does adding a high score change how you play?
- Did anything break in surprising ways while you were building it? This is the most important question. Programmers spend most of their time fixing things that broke. Your stories matter.
You did something today that you couldn’t have done six weeks ago. You took a blank Scratch project and built a complete game that has a goal, a challenge, scoring, and a way to lose. Every video game you’ve ever played has those four things. You now know how to put them in something you built.
Looking ahead to your own project
Next week we start your milestone project — your game, your idea, your design. You’ll have two weeks to plan, build, and polish, then you’ll show it to the class.
Start thinking now: what do you want to make? Some ideas to get you started:
- A different kind of catch game (catching different things, different objectives)
- A maze game where the cat has to reach a goal without touching the walls
- A drawing toy (using the pen extension from Session 3)
- A mini-quiz where the cat asks questions and reacts to clicks
- A “boss battle” where you click a sprite many times to defeat it
- An interactive story — multiple sprites, multiple scenes, the user clicks to advance
- Anything else you can imagine that uses what you’ve learned
You don’t have to decide today. But come to next week’s class with at least one idea you’re excited about.
If you missed this session
Open Scratch and start a new project. Then:
- Build the cat with smooth left/right movement at the bottom of the stage. Reset score to 0 on green flag. (Part A above.)
- Add an apple that falls, gives a point on catch, resets either way.
- Add a rock that falls. If it touches the cat:
say "Game Over" for 2 seconds, thenstop [all]. If it falls off the bottom: reset to top. - Test it. Catch apples. Try to dodge the rock.
About 30-40 minutes of work.
Then think about what you want to build for your own project next week. Bring an idea.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Add a backdrop (bottom-right Choose a Backdrop button) so the game has a setting — a forest, a city, outer space.
- Add a sound effect when the cat catches an apple. (Sound
category,
start sound. Some apple sprites come with sound effects built in.) - Make the cat change costume when game over fires (look at the Costumes tab — many cats have multiple costumes).
- Add a third “good” sprite (a star, a heart) worth more points than the apple. Now you have to choose what to chase.
What’s next
Next week is the start of your milestone project — two weeks to plan, build, and polish your own game. Bring an idea or two. The final week will be a class demo day where everyone shows what they built.
Session 8: Milestone project work day 1
Session 8: Milestone project work day 1
Phase 1 — Scratch · Session 8 of 9
What we’re learning today
Today is your day. You’re going to plan a game of your own choosing, then start building it. There’s no new programming concept this week — instead, you’ll use everything you’ve learned to make something that’s yours, not Mr. Eric’s. Next week you’ll finish it and show it to the class.
You’ll need to remember from last time
- Everything from Sessions 1-7. No, really. Sequences, loops, events, conditionals, sensing, variables. You’ll use all of them.
- Apples and Rocks from Session 7 — proof that you can build a complete game. If you built it, you can build something else too.
- The idea you brought with you today. (If you didn’t bring one, that’s okay — Part A will help.)
Part A: Plan your game
The most important thing a programmer does isn’t typing code. It’s deciding what to build before they start. Real software projects fail far more often because nobody decided what they were building than because the code was hard. Today you’ll do the planning step.
The plan
Take a piece of paper (or open a text editor). Answer these questions about your game. Don’t think too hard yet — first answers are usually fine.
-
What’s the name of your game? (Make one up. You can change it later. Examples: “Donut Drop,” “Cat vs. Asteroids,” “The Royal Quest.”)
-
In one sentence, what is your game? (Try to fit it on one line. Example: “You’re a wizard catching falling books to build a tower.”)
-
What sprites do you need? (List them. Example: “Wizard sprite, three book sprites, a tower-base backdrop.”)
-
What does the player do? (How do they control things? Arrow keys? Click sprites? WASD? Some combination?)
-
What’s the goal? (How does the player know they did well? Score that goes up? Reach a place? Survive a long time?)
-
What can go wrong? (How does the game end, or get harder? Game over? Missed too many? Time runs out?)
-
What’s the simplest version that’s still fun? (This is the most important question. If you only had today and next week to build this, what would you do FIRST? What would you add LATER if there’s time?)
Talk it over with your buddy
Spend 5 minutes telling your buddy about your plan. Then your buddy tells you about theirs. Each of you says one thing about the other’s plan — could be a question (“how does the player restart?”) or a suggestion (“you could add a sound when they win!”) or just an observation (“that sounds fun”). One thing. Don’t redesign each other’s games.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question that helps you tighten it up. If he asks a question, that’s not a bad thing — it means there’s something worth thinking about for two more minutes.
Checkpoint: You have a written plan with all seven questions answered, and Mr. Eric has signed off on it. This is the natural stop point if class is cut short — you can start building next week, and you’ve done the most important thing already by planning.
Part B: Start building
Now you build. The rest of class is yours.
Where to begin
Don’t try to build everything at once. Real programmers build the simplest version that works first, then add to it. Look at your answer to question 7 above — that’s where to start.
If your game is “wizard catches falling books,” the simplest version is:
- Wizard sprite that moves with arrow keys.
- One book sprite that falls.
- When the wizard touches the book, score goes up.
That’s it. Then add a second book. Then add a way to lose. Then add the tower visual. Then add sounds. Each step works before the next one starts.
This is called iterative building and it’s the secret most programmers wish they’d learned earlier. Build small. Test it. Build more. Test it. Repeat.
When you get stuck
When (not if) you get stuck:
- Read your code carefully. Did you tell the computer the exact thing you meant? (Remember the peanut butter problem.)
- Ask your buddy. They might see something you don’t.
- Look at how you did something similar before. If the apple in Apples and Rocks worked the way you want, copy that pattern.
- Ask Mr. Eric. Last resort, after the first three.
Getting stuck is normal. Programmers spend most of their time stuck. The skill isn’t avoiding stuck — it’s getting unstuck.
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today in your project. Could be a sprite that moves. Could be a score that updates. Could be a backdrop that loads. Whatever’s working — that’s a win.
Don’t worry if you didn’t get as far as you wanted. Most projects take longer than the plan said they would. That’s why the plan asked about the simplest version first — so you’d have something working even if you ran out of time.
Bring your project file (or just your machine if it’s your class computer) next week. We’ll finish, then demo to the class.
If you missed this session
Open Scratch and start a new project. Then:
- Spend 10-15 minutes answering the seven planning questions above on paper or in a text file.
- Start building the simplest version of your game (look at your answer to question 7).
- When you come next week, you’ll be ready to keep building.
If you don’t have an idea, here are some starting points:
- A different falling-things catcher (catching donuts, coins, letters of the alphabet)
- A maze game where the player has to reach a target
- A pet care game (click the sprite to “feed” it; a hunger variable goes down over time)
- A whack-a-mole game (a sprite appears at random spots; click it before it disappears)
- A tiny choose-your-own-adventure (sprite asks a question, you click left or right side of the screen, the story branches)
Any of those can be built with what you know. Pick one and modify it — make it your own. Don’t just copy.
Stretch and extension ideas
If your base game is working and you want to add more:
- A second screen — use a backdrop change at game over so the player sees a different background.
- Multiple difficulty levels — a variable that controls how fast things fall, increased by score.
- A start screen — when the green flag is clicked, show instructions until the player presses space, then start the game.
- Sound effects for important moments (catch, miss, game over).
- A costume change for any sprite at an interesting moment.
Whatever you add, add it one piece at a time and test after each piece. Big changes that touch many things are how games break.
What’s next
Next week you’ll have time to finish, polish, and demo your game to the rest of the class. Each person will get 3-5 minutes. Don’t worry — it’s a friendly demo, not a test. Bring a working game, bring your enthusiasm, and the rest takes care of itself.
Session 9: Milestone project work day 2 + demo day
Session 9: Milestone project work day 2 + demo day
Phase 1 — Scratch · Session 9 of 9
What we’re learning today
Today is the last session of Phase 1. You’ll spend the first half finishing and polishing your game — fixing bugs, adding the last features, making sure it runs. Then in the second half, you’ll demo your game to the class. Everyone will see what everyone else made. By the end of class, you’ll have shipped your first programming project.
You’ll need to remember from last time
- Your project plan from Session 8.
- Whatever you got working last week — the foundation we’ll finish on today.
- Your buddy — they’ll help you test your game one last time before the demos.
Part A: Final polish
You have about 30-40 minutes to finish your game. Be honest with yourself about what’s left to do.
What “finished” means
A finished milestone project is:
- It runs. You can click the green flag and play it without crashing or getting confused.
- It does what your plan said it would do — at least the base version (your answer to question 7 from Session 8).
- You can explain how it works. If someone asks “how does the score get updated?” you can walk them through it. (Remember the “explain how it works” rule.)
If your game doesn’t meet all three of those, focus on getting it there before adding anything new. A working basic game is way better than a broken ambitious one.
Check it with your buddy
About 15 minutes in, swap with your buddy. They play your game for two minutes; you play theirs. Notice:
- Did it work without help?
- Did anything break or do something unexpected?
- Could they tell what to do without instructions?
Tell each other what you noticed. This is the most useful feedback you’ll get on your project, because your buddy is seeing your game with fresh eyes.
Remember the peanut butter
It’s been eight weeks since the first session. Take a moment to remember:
Way back in Session 1, you couldn’t even tell Mr. Eric how to make a peanut butter and jelly sandwich. The computer is just like Mr. Eric was that day — except the computer doesn’t know what bread is.
You spent eight weeks getting better and better at telling that extremely stupid (and extremely literal) computer exactly what to do. Today, you’re going to show the room a game that you built — that runs because you gave the computer instructions precise enough to follow.
That’s a big deal. Sit with that for a second.
Checkpoint: Your game runs the basic version of what you planned, and you and your buddy have each played each other’s games once. This is the natural stop point if class is cut short — but for today, we’re going to keep going to the demo half.
Part B: Demo day
This is the fun part. Each of you will get 3-5 minutes to demo your game to the class.
How a demo works
When it’s your turn:
- Show your game. Play it for a minute or two. Let people see what it does. (You can also let someone else play it if you want.)
- Tell us about it. What’s it called? What were you trying to make? What’s the goal of the game?
- Tell us one thing that was hard. Not bragging — just honest. “Getting the score to reset was tricky.” “I couldn’t figure out how to make the rock spawn at random spots at first.” Things that broke and how you fixed them.
- Take one question. Someone in the room will ask one question — about how you built it, why you chose what you chose, what you’d add next. Answer it as best you can. “I don’t know, I just tried things until it worked” is a great answer.
How to be a good audience
When it’s not your turn:
- Watch. Pay attention to what other people made.
- Be ready to ask one question when it’s your turn to ask. Good questions are specific — “how did you make the dragon breathe fire?” is way better than “that was cool, how’d you make it?”
- Be encouraging. Mr. Eric will say something specific about each project at the end, and you can do the same as a buddy comment. “I really liked the part where the music changed when you scored” is way more useful than “good job.”
After everyone demos
Mr. Eric will say a few things about each of you and what he saw across the projects. Then we’re done — Phase 1 is over.
What you accomplished today
- You shipped a project. You took an idea, planned it, built it, and showed it to people. Most adults never do that. You just did it twice (once with Apples and Rocks, once with your own game).
- You gave a demo. Talking about what you made — what you intended, what was hard, what you’d do next — is a real career skill. Engineers do this every week. You just did it.
- You finished Phase 1. That’s eight weeks of programming. You know sequences, loops, events, conditionals, sensing, variables, and how to combine them all into something playable. That’s a real foundation.
What’s next
Next week we start Phase 2 — Python. Scratch is great for learning the ideas of programming, but it’s not what most programmers actually use day-to-day. Phase 2 introduces you to Python, a real programming language that grown-ups use to write everything from games to websites to scientific tools.
The first Python session uses something called Turtle — it’s a tool where you write Python code and a little arrow on the screen draws shapes for you. Familiar territory (drawing with code, like the pen extension in Session 3) but in real Python syntax.
You’ll start typing instead of dragging blocks. It feels different at first. That’s okay. The thinking is the same — sequences, loops, conditionals, all of it carries over. The peanut butter rule definitely carries over.
Bring your curiosity. See you next week.
If you missed this session
Two cases:
If you missed only the demo half: Show your game to Mr. Eric at the start of next week’s class. He’ll give you the same kind of feedback the demo would have. You won’t have missed anything content-wise.
If you missed the whole session and don’t have a finished project: No big deal. You can finish your project at home (reference the Session 8 plan you wrote) and bring it next week. Or just join us in Phase 2 — your Apples and Rocks game from Session 7 already counts as a project you built.
Stretch and extension ideas
If you have time after the demos and want to keep going:
- Polish your game more. Add a backdrop, sounds, a start screen. Make it look nice.
- Build a second game. With everything you know now, the second game is faster than the first. Try a different style — if you made a catcher, try a maze. If you made a clicker, try a dodger.
- Show your game to your family. They will be impressed. This is a fact.
- Save your project file. You’ll want it later. By the time you finish Phase 8, you’ll be amazed how far you came.
What’s next
Next week starts Phase 2: Python with Turtle. Real code, real syntax, same thinking. Bring your machine and your brain.
Phase 2 — Python with Turtle
You finished Phase 1. You built a game in Scratch. You can think like a programmer. Now we switch to a real programming language: Python.
What this phase is
Phase 2 is the bridge between Scratch and the rest of the curriculum. The big shift this phase is typing instead of dragging. The blocks are gone. You’ll type real Python code into a real editor, save it as a real file, and run it.
To make the shift gentler, we’re not jumping straight into “all of Python.” Instead, we use a tool called Turtle — it’s part of Python itself, and it lets you draw on the screen by giving commands to a little arrow (a turtle). Like the pen extension in Scratch, but in Python.
By the end of Phase 2, you’ll be writing real Python programs that draw real things, and the typing-not-dragging shift will feel normal. After this phase, the rest of the curriculum is all in Python.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to Python (typing, not dragging) | Open Thonny, your first line of Python, your first error message |
| 2 | Telling the turtle what to do | More turtle commands; coordinate-based positioning |
| 3 | Loops in Python | for i in range(N): — the Python version of repeat |
| 4 | Functions | The big new idea: making your own commands |
| 5 | Variables and a little math | x = 5 syntax; arithmetic in code |
| 6 | Conditionals: if in Python | if x > 5: — the Python version of the if block |
| 7 | Putting it together — a drawing project | Combine everything into a creative pattern |
| 8 | Milestone project + demo | Plan, build, demo a Turtle drawing of your own |
A lot of these ideas (sequences, loops, conditionals, variables) are familiar from Phase 1. What’s new is the syntax — how you type each idea in Python. The biggest brand-new idea is functions in Session 4. We saved that one for Python on purpose.
What you’ll build
Each session ends with a small drawing — shapes, scenes, patterns. By Session 7 you’ll be making creative compositions that combine loops, functions, and color in ways that would have been impossible with Scratch’s pen extension.
The milestone project in Session 8 is your own Turtle drawing — a pattern, a picture, a scene, a name in big letters, anything you can imagine. You’ll plan it, build it, and demo it to the class.
What you’ll need
- The same machine you used in Phase 1.
- Thonny — the Python editor. Pre-installed on your class computer. If you’re working at home, the Installing on your own computer appendix tells you how to get it.
- The turtle module — comes with Python. No install needed.
- Patience. Typing precise code feels weird at first. It gets better fast.
How sessions work
Same shape as Phase 1:
- Part A introduces a new idea with a guided exercise. Stands alone if class gets cut short.
- Part B is open practice or a project. Often tiered (base / stretch / extension).
- Wrap-up to share what you did.
What’s a little different in Phase 2: there will be error messages. A lot of them. Read them slowly. They’re trying to help. The Reading error messages appendix will get fleshed out as we encounter common ones.
A note about typing
If you’re a slow typist, that’s okay. Programming isn’t about typing speed — it’s about thinking clearly. Most professional programmers type around the same speed as a careful average person. What matters is being precise. A typo is a bug, and fixing typos is a real part of programming.
If your typing feels slow enough to be frustrating, mention it to Mr. Eric. He can suggest some ways to practice between classes. But it’s not a barrier — kids with all kinds of typing speeds finish this curriculum.
Where to start
Session 1: Welcome to Python — typing instead of dragging is where Python begins.
When you’re stuck, the Getting unstuck appendix has a checklist that helps. The Glossary explains vocabulary; Python words will start showing up there as we encounter them.
Welcome to typing. Let’s go.
Session 1: Welcome to Python — typing instead of dragging
Session 1: Welcome to Python — typing instead of dragging
Phase 2 — Python with Turtle · Session 1 of 8
What we’re learning today
You spent eight weeks learning how to think like a programmer using Scratch. Today we move into a real programming language called Python — the kind of language used by people who write apps, games, and websites for a living. The blocks are gone. You’ll be typing code instead of dragging it. By the end of class, you’ll have typed your first Python program, watched a turtle draw on your screen, and made (and fixed!) your very first error message.
You’ll need to remember from last time
- Sequences — code runs top to bottom. Order matters.
- The peanut butter rule — computers do exactly what you say, not what you mean. (This rule applies more to Python, not less. You’ll see why.)
- That you finished Phase 1 and built your own game. You can do hard things.
Part A: Meet Thonny and your first turtle
Open Thonny on your computer. It’s already installed — look for a friendly green icon (sometimes it looks like a snail). If you can’t find it, ask your buddy or Mr. Eric.
Thonny is what we’ll use instead of Scratch. It’s called an IDE (which stands for Integrated Development Environment, but nobody says that out loud). For now, it’s the place where you’ll type your Python code.
A tour of Thonny
When Thonny opens, you’ll see two main areas:
- The editor (the top half, big white area). This is where you type your code.
- The shell (the bottom half). This is where Python tells you things — what your program printed, what error it ran into, things like that.
At the top there’s a row of buttons. The one we care about today is the green Run button (or you can press the F5 key on your keyboard — same thing).
Type your first Python program
Click into the editor area at the top. Type this — exactly:
import turtle
t = turtle.Turtle()
t.forward(100)
Three lines. Pay attention to the symbols — the parentheses, the
period after t, the period after turtle. Type them all. Type
them carefully.
Now save your file. Press Ctrl-S (or File → Save). Thonny
will ask where to save it and what to call it. Save it somewhere
you’ll remember (the desktop is fine for now), and call it
first_turtle.py. The .py part is important — that’s how
your computer knows it’s a Python file.
Now click the green Run button (or press F5).
A new window pops up — it’s mostly white, with a small arrow-shape in the middle. That arrow is your turtle. Watch what happens: the turtle moves forward 100 pixels and stops.
Congratulations. You just wrote and ran a Python program.
What just happened
Let’s break those three lines down:
import turtle— this tells Python: “I want to use the turtle toolbox.” Python comes with lots of toolboxes; you have to ask for the ones you want. We just asked for the one that does drawing with a turtle.t = turtle.Turtle()— this creates a new turtle and gives it the namet. (You can name it anything you want, buttis short and quick to type. Some people name itbob. Some name itgeorge. The turtle doesn’t care.)t.forward(100)— this tells the turtle namedtto move forward by 100 pixels. The number 100 is called an argument — it’s the information you’re passing to the command.
Compare that to Scratch:
| Scratch | Python |
|---|---|
Drag a move 10 steps block | Type t.forward(100) |
| The cat moves | The turtle moves |
Same idea. Different way of telling the computer.
Make a typo on purpose
Now let’s break it. Add a fourth line, but spell forward wrong:
import turtle
t = turtle.Turtle()
t.forward(100)
t.forwerd(100)
Click Run.
The turtle does the first forward. Then it stops, and the shell at the bottom turns red and shows you something like this:
AttributeError: 'Turtle' object has no attribute 'forwerd'
This is your first error message. Don’t panic. Error messages are trying to help. Read it carefully.
Translation: “I tried to find a thing called forwerd for the
turtle, but the turtle doesn’t have anything called forwerd.
You probably meant something else.”
Fix the typo (change forwerd back to forward) and run again.
The error goes away. The turtle moves twice.
The peanut butter rule strikes again. You meant
forward. You typedforwerd. The computer doesn’t know what you meant — it only knows what you wrote. Welcome to typing precision.
Draw a square
Now let’s make the turtle draw a real shape. Replace your code with this:
import turtle
t = turtle.Turtle()
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
Save (Ctrl-S). Run (F5).
The turtle walks forward 100, turns right 90 degrees, walks forward 100, turns again, four times. A square.
Look familiar? In Session 3 of Phase 1, you made the cat draw a
square in Scratch with repeat 4. We don’t have repeat yet in
Python — that’s next week. For now, four times by hand.
Checkpoint: You have a Python program saved as
first_turtle.py that draws a square when you press Run. This
is the natural stop point if class is cut short.
Part B: Colors, pen control, and shapes
Now let’s make our drawings look better.
Colors
Add this line before you start drawing the square:
t.color("red")
Save and run. The square is red.
Try other colors: "blue", "green", "purple", "orange",
"pink", "yellow", "black". Anything you’d guess, Python
probably knows.
Thicker lines
Add this line too:
t.pensize(5)
Now the lines are five pixels thick instead of one. Try pensize(10)
or pensize(20).
Lifting the pen
Sometimes you want to move the turtle without drawing. Use these:
t.penup()— lift the pen, no more drawingt.pendown()— drop the pen, drawing again
Try this — two squares side by side:
import turtle
t = turtle.Turtle()
t.color("blue")
t.pensize(3)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.penup()
t.forward(150)
t.pendown()
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
Two squares with a gap between them. The middle three lines (penup, forward 150, pendown) move the turtle without drawing.
Try this on your own
Pick one of these:
- Base goal: Draw a triangle. (A triangle is three sides. The turn at each corner is not 90 degrees — figure out what it should be. Hint: remember the polygon pattern from Session 3 of Phase 1.)
- Stretch: Draw your initials. Each letter as a few lines.
Use
penup()andpendown()to skip between letters. - Extension: Draw a simple house. A square for the body, a triangle for the roof on top. Use color.
Wrap-up
Before we leave, let’s share:
- What was different from Scratch?
- What was harder?
- What was easier?
- Did you get any error messages? What were they?
You did something today that’s a big deal. You typed real Python code, ran it, made a typo, fixed it, and made the computer draw something. That’s the entire programming workflow. Every programmer in the world does that exact loop, every day.
The hard part wasn’t the typing. The hard part was getting used to being precise — every character matters, every parenthesis matters, every period matters. That precision will get easier fast.
If you missed this session
Open Thonny on your computer. Type these three lines exactly:
import turtle
t = turtle.Turtle()
t.forward(100)
Save the file as first_turtle.py. Click the green Run button or
press F5. A window opens; the turtle moves.
Now extend it to draw a square. You’ll need four t.forward(100)
lines and four t.right(90) lines, alternating.
Then add t.color("red") and t.pensize(3) near the top to make
the square colorful and thick. Save, run.
About 30 minutes of work. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Try
t.left(90)instead oft.right(90)and see what happens. - Try
t.backward(50)to walk the turtle backward. - Try
t.shape("turtle")(right after thet = ...line). The arrow becomes an actual little turtle shape. Then try"square","circle","triangle","arrow". - Try
t.speed(1)(slow) ort.speed(10)(fast). Where does the speed matter? Where doesn’t it?
What’s next
Next week we’ll learn about all the things the turtle can do — moving in different ways, going to specific positions, drawing patterns. By the end of next week’s class, your drawings will look way fancier.
Session 2: Telling the turtle what to do
Session 2: Telling the turtle what to do
Phase 2 — Python with Turtle · Session 2 of 8
What we’re learning today
Last week you typed your first Python code and made a turtle draw a square. Today we’ll learn more things the turtle can do — jump to specific spots, face specific directions, draw circles, and even write text. By the end, you’ll be able to draw a small scene with multiple parts: a sun, a house, a tree.
You’ll need to remember from last time
- Open Thonny, type code in the editor, save with a
.pyextension, click Run (or press F5). - Every Python turtle program starts with
import turtleandt = turtle.Turtle(). t.forward(N),t.right(N),t.left(N)for movement.t.color("red")andt.pensize(N)for style.t.penup()andt.pendown()for skipping without drawing.- Sequences run top to bottom. Order matters in Python just like it did in Scratch.
Part A: More ways to tell the turtle what to do
Open Thonny, start a new file, and save it as scene.py. Type
the standard opening:
import turtle
t = turtle.Turtle()
You’ll add to it.
Jumping to a specific spot: goto
In Scratch, you used go to x: 0 y: 0 to send a sprite to a
specific position. Python turtle has the same idea. The stage has
a coordinate system: the middle is (0, 0), x goes right
(positive) and left (negative), y goes up (positive) and down
(negative).
Add this to your file:
t.penup()
t.goto(100, 50)
t.pendown()
t.forward(50)
Save. Run.
The turtle lifts its pen, jumps to the point (100, 50), drops the pen, and walks 50 forward from there.
You’ll use goto constantly when you have multiple things to draw
at different positions. Lift the pen, jump, drop the pen, draw.
Facing a specific direction: setheading
After goto, your turtle is somewhere, but which way is it
facing? It’s facing wherever it was facing before. That’s often not
what you want.
setheading makes the turtle face a specific direction:
t.setheading(0)— face right (east)t.setheading(90)— face up (north)t.setheading(180)— face left (west)t.setheading(270)— face down (south)
So if you want to draw something that points up, you do:
t.penup()
t.goto(0, 0)
t.setheading(90) # face up
t.pendown()
t.forward(80) # draw an upward line
Use goto and setheading together when you need to start
drawing in a known place at a known angle. Most multi-part drawings
need both.
Circles
The turtle can also draw circles. Add this:
t.penup()
t.goto(-150, 100)
t.pendown()
t.color("yellow")
t.pensize(3)
t.circle(40)
Save. Run. A yellow circle of radius 40 appears in the upper-left area.
t.circle(40) draws a complete circle. The circle starts where
the turtle currently is, and the turtle ends up back at that same
spot when the circle is done.
You can also draw a partial circle by giving a second argument
— how many degrees of arc to draw. t.circle(40, 180) draws a
half-circle.
Writing text
The turtle can write text too. Add:
t.penup()
t.goto(0, 150)
t.pendown()
t.color("black")
t.write("My Scene", font=("Arial", 20, "normal"))
Save. Run. The text “My Scene” appears at the top of your window.
The font=("Arial", 20, "normal") part says what font and size
to use. Don’t worry about the syntax for now — just know you can
change 20 to make the text bigger or smaller.
Build a scene together
Let’s combine everything into a small scene. Start fresh — delete your code and type this:
import turtle
t = turtle.Turtle()
# the sun
t.penup()
t.goto(150, 100)
t.pendown()
t.color("yellow")
t.pensize(2)
t.circle(30)
# the house body (a square)
t.penup()
t.goto(-50, -50)
t.setheading(0)
t.pendown()
t.color("brown")
t.pensize(3)
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
t.forward(100)
t.left(90)
Save. Run.
You should see a yellow sun in the upper right and a brown square (house) below it.
Notice the lines starting with # — those are comments.
Anything after a # on a line is ignored by Python. Comments are
how you leave notes for yourself (or other readers) explaining what
each part of your code does. We’ll use them more as our programs
get bigger.
Checkpoint: You have a scene with at least two distinct elements (a sun and a house, or similar) drawn at different positions on the stage. This is the natural stop point if class is cut short.
Part B: Make it your scene
Now make this scene yours. The base goal is small; the stretch adds to it; the extension lets your imagination loose.
Base goal
Add a third element to your scene. Pick one:
- A roof on top of the house. (Triangle. Use
gototo position the turtle at one corner,setheadingto face the right way, then threeforward/leftpairs to draw the triangle. Don’t forget the third turn at the end so the turtle’s heading is predictable for whatever comes next.) - A tree. A brown rectangle for the trunk, a green circle for the leaves on top.
- A second sun of a different color, in another corner.
- A title. Use
t.write()to put your name or a title at the top of the scene.
Stretch
Add two more elements, of any kind. Mountains in the background. A second house. A road. A door on the existing house. Something that makes the scene feel like a place.
Start using comments (# something) to label what each part of
your code is. It’ll help you find things when the program gets
longer.
Extension
Tell a tiny visual story. Add elements that suggest something is happening — a second person/animal, a path, smoke from the house’s chimney, weather (clouds, rain, sun rays). Use as many turtle commands as you can.
Some commands you haven’t formally seen but might want:
t.circle(radius, extent)— partial circlet.shape("turtle")— make the cursor look like a little turtlet.speed(0)— turn the turtle drawing animation off (way faster)t.hideturtle()— hide the cursor entirely after drawing
Try them. The “If you finish early, just try things and see what happens” energy is exactly the right mindset.
Wrap-up
Before we leave, share with the room:
- What’s in your scene?
- What was the trickiest part to position correctly?
- Did anyone discover a turtle command we didn’t talk about?
You did something interesting today: you used multiple parts of your code to build one thing. The sun, the house, the tree — each a separate sequence of commands, all together making one drawing. That’s how every real program works. Big programs are made of small pieces, each doing one thing.
Your scene is also probably long now — maybe 40 or 50 lines. Some
of those lines repeat. Like the four pairs of forward(100) /
left(90) to draw the square. Wouldn’t it be great if you could
say “draw a square” instead of typing those eight lines?
You can. That’s next week.
If you missed this session
Open Thonny and start a new file. Save it as scene.py. Then:
- Build the standard opening (
import turtleandt = turtle.Turtle()). - Practice with
goto:t.penup(),t.goto(100, 50),t.pendown(),t.forward(50). Run and see the turtle jump. - Practice with
setheading:t.setheading(90),t.forward(50). The turtle now draws a line going up. - Practice with
circle:t.color("yellow"),t.circle(40). The turtle draws a yellow circle. - Now build the scene from Part A above (sun + house). Then add one more element (Part B base).
About 30-40 minutes of work. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Use
t.fillcolor("red")and surround a shape witht.begin_fill()/t.end_fill()to fill it with color, not just outline. - Use
t.bgcolor("skyblue")to set the background color of the whole window. - Use multiple turtles. After
import turtle, dot = turtle.Turtle()ands = turtle.Turtle(). Now you have two turtles you can control independently. (Hint: this gets way more useful when we have loops next week.)
What’s next
Next week we finally get loops in Python. Remember when you typed eight lines to draw a square last week, and your house this week? Next week you’ll do it in three. By the end of class, you’ll be drawing patterns that would take hundreds of lines by hand.
Session 3: Loops in Python — for and range
Session 3: Loops in Python — for and range
Phase 2 — Python with Turtle · Session 3 of 8
What we’re learning today
Last week your code was getting long and repetitive — eight lines
to draw a square, lots of similar lines for the scene. Today
that’s about to change. We’ll learn the Python version of Scratch’s
repeat block, called a for loop. By the end of class, you’ll
draw a square in three lines instead of eight, and you’ll be
making patterns that would take hundreds of lines without loops.
You’ll need to remember from last time
- Open Thonny, type code, save with
.py, click Run. - Every program starts with
import turtleandt = turtle.Turtle(). t.forward(N),t.right(N),t.left(N),t.color(),t.pensize().#starts a comment. Everything after it on the line is ignored.- Loops from Scratch (
repeat 4,forever). Same idea, new syntax.
Part A: The for loop
Open Thonny and start a new file. Save it as loops.py.
The square, the long way
Let’s start by typing the square the way we did in Session 1:
import turtle
t = turtle.Turtle()
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
t.forward(100)
t.right(90)
Run it. A square. Eight lines.
Now look at those eight lines. Two lines repeated four times. That’s exactly what a loop is for: doing the same thing N times without writing it N times.
The square, the loop way
Replace your eight lines with this:
import turtle
t = turtle.Turtle()
for i in range(4):
t.forward(100)
t.right(90)
Run it. Same square. Three lines instead of eight.
Let’s break down what’s happening:
for i in range(4):— this means “do the next part 4 times.” Therange(4)produces a sequence of numbers from 0 to 3 (which is four numbers — Python starts counting at 0, not 1).iis the loop variable. Each time through the loop,itakes the next value from the sequence (0, then 1, then 2, then 3). We don’t have to useiin this loop — but the syntax requires we name it. Most programmers useiby default.- The colon at the end of the
forline is required. Don’t forget it. - The lines underneath the
forare indented — pushed in by four spaces. That’s how Python knows they’re part of the loop.
Indentation matters in Python
This is the big new thing about Python that didn’t exist in Scratch. Indentation is part of the grammar.
In Scratch, you snapped blocks inside the repeat block’s mouth.
Python uses indentation instead. Lines that are indented under
for i in range(N): are inside the loop. Lines that are not
indented are outside the loop.
Try this — change the file so the second line isn’t indented:
for i in range(4):
t.forward(100)
t.right(90)
Now run it. The turtle moves forward 4 times in a straight line,
then turns once. That’s because only t.forward(100) is inside
the loop. The t.right(90) is outside — it runs once, after
the loop is done.
Indentation isn’t decoration. It’s part of what your program does. Be careful with it.
Good news: Thonny indents for you. When you type
for ... :and press Enter, Thonny moves your cursor in 4 spaces automatically. Trust it.
Heads up: If you ever copy-paste code from somewhere, the indentation may not match. Mixed tabs and spaces is a common cause of confusing errors. If your code looks right but won’t run, check the indentation by clicking each line and looking at the cursor position.
Try other shapes
Now try changing the loop count and the turn angle:
for i in range(3):
t.forward(100)
t.right(120)
Run it. A triangle.
Try range(5) and t.right(72). A pentagon.
Try range(6) and t.right(60). A hexagon.
You probably noticed: number-of-sides times turn-angle always equals 360. Same pattern as Phase 1 Session 3. Loops just made it way easier to test.
Checkpoint: You’ve replaced a manually-typed shape with a
for loop, and you’ve used range(N) with a different number to
draw at least one different shape (triangle, pentagon, etc.).
This is the natural stop point if class is cut short.
Part B: Patterns that loops make easy
Now we’ll do things that would have been ridiculous to type by hand.
A spiral
Start a new file. Save it as spiral.py. Type:
import turtle
t = turtle.Turtle()
t.speed(0)
t.color("blue")
for i in range(50):
t.forward(i * 2)
t.right(15)
Run it. A blue spiral that grows outward.
Look at what’s happening: each time through the loop, i is
different (0, 1, 2, 3, … 49). And i * 2 (i times 2) is the
distance the turtle walks. So the first time the turtle walks 0
steps, then 2, then 4, then 6… longer and longer. Combined
with a 15-degree turn each time, it spirals.
This is the first time we’ve actually used the loop variable
i for something. It’s not just a counter — it’s a value we can
compute with. Variables are next week’s lesson, so don’t worry
about the math yet — just notice that i does something.
t.speed(0) makes the turtle draw without animation (instant).
On a long loop like this, the animation would take forever.
Try the base goal
Pick one:
- A colorful shape: Use a loop to draw a many-sided shape
(like a 12-sided dodecagon, with
range(12)andt.right(30)). Make it any color you want. - A row of squares: Use a loop to draw 5 squares in a row, each one a small distance apart. (Hint: inside the loop, draw a square the long way — without a loop — then move the turtle to the next position.)
Stretch — nested loops
You can put a loop inside another loop. The inside loop runs all the way through, then the outside loop moves to its next iteration, then the inside loop runs all the way through again, and so on.
Try this — the flower of squares from Phase 1:
import turtle
t = turtle.Turtle()
t.speed(0)
t.color("purple")
for i in range(12):
for j in range(4):
t.forward(50)
t.right(90)
t.right(30)
Run it. A flower-shaped pattern made of 12 rotated squares.
The outer loop runs 12 times. Each time, the inner loop draws a square (4 sides), and then the outer loop turns 30 degrees. 12 squares × 30 degrees = 360 degrees, so they go all the way around.
Notice: the inner loop’s body is indented twice (8 spaces from
the left). Indentation level says how nested you are. The
t.right(30) is indented once (4 spaces) because it’s inside the
outer loop but outside the inner loop.
This is genuinely confusing the first time. Read it carefully. Look at the indentation. Run it. Change the numbers and see what changes.
Extension
Make a colorful spiral that changes color as it grows. Add this to the spiral:
import turtle
t = turtle.Turtle()
t.speed(0)
colors = ["red", "orange", "yellow", "green", "blue", "purple"]
for i in range(60):
t.color(colors[i % 6])
t.forward(i * 2)
t.right(15)
The colors[i % 6] part picks a color from the list based on
which iteration we’re in. The % operator gives the remainder
when dividing — so i % 6 cycles through 0, 1, 2, 3, 4, 5, 0,
1, 2, 3… and that picks the colors in order, repeating.
If that’s confusing right now, that’s fine. Just type it and run
it. Lists, indexing, and the % operator are things we’ll cover
properly in later sessions. For now, enjoy the rainbow.
Wrap-up
Before we leave, share with the room:
- What was the most-sides shape anyone drew?
- Did anyone try the nested-loops flower? What did it look like?
- Did anyone get a confusing indentation error? What fixed it?
You did something today that genuinely changes what you can build. Loops are how programmers make patterns, animations, and anything that involves “do this many times.” From here on out, you’ll use them constantly.
You also met indentation as a real grammar rule. That’s the single weirdest thing about Python compared to most other programming languages. After a couple of sessions it’ll feel normal — for now, just remember: indented = inside, not indented = outside.
If you missed this session
Open Thonny and start a new file. Save it as loops.py. Then:
-
Build a
forloop that draws a square:import turtle t = turtle.Turtle() for i in range(4): t.forward(100) t.right(90)Save, run. Watch the square draw.
-
Change
range(4)andt.right(90)to draw a triangle (3 sides, 120 degrees) and a pentagon (5 sides, 72 degrees). -
Try the spiral from Part B above.
About 30 minutes. Pay attention to indentation — the lines inside the loop need to be pushed in by 4 spaces (Thonny does this for you when you press Enter after the colon).
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Try
range(N)with a different starting number:range(5, 15)produces 5 through 14.range(0, 20, 2)produces 0, 2, 4, 6, …, 18 (counts by twos). - Try a
for i in range(360)loop where the turtle moves 1 step and turns 1 degree each iteration. What shape does that draw? - Try multiple loops in one program. Draw five different shapes,
one after another, with
penup/gotobetween them to space them out.
What’s next
Next week we’ll learn about functions — how to take a chunk
of code (like “draw a square”) and give it a name, so you can
just say draw_square() instead of writing the whole loop every
time. It’s the biggest new idea in Phase 2.
Session 4: Functions — making your own commands
Session 4: Functions — making your own commands
Phase 2 — Python with Turtle · Session 4 of 8
What we’re learning today
Today is the biggest new idea of Phase 2. You’ll learn how to take
a chunk of code and give it a name — so that instead of
copying the same code over and over, you just call it by name. A
named chunk of code is called a function, and they’re how
real programs get built. By the end of class, you’ll have written
your own draw_square and draw_house commands and used them
to build a tiny neighborhood.
You’ll need to remember from last time
for i in range(N):— runs the indented body N times.- Indentation matters. Lines indented under a
forare inside the loop. t.color(...),t.pensize(...),t.penup(),t.pendown(),t.goto(x, y),t.setheading(angle).
Part A: Define your own command
Open Thonny and start a new file. Save it as functions.py.
The motivation
Suppose you want to draw three squares at three different spots
on the stage. With what you know so far, you’d write the same
loop three times, with goto calls between them:
import turtle
t = turtle.Turtle()
# square 1
for i in range(4):
t.forward(60)
t.right(90)
t.penup()
t.goto(-100, 0)
t.pendown()
# square 2
for i in range(4):
t.forward(60)
t.right(90)
t.penup()
t.goto(100, 0)
t.pendown()
# square 3
for i in range(4):
t.forward(60)
t.right(90)
Three squares. Lots of typing. The same loop, copied three times. We just learned loops to avoid copying things — and now we’re copying things again.
There’s a better way.
Functions: chunks of code with names
A function is a piece of code that you give a name to. Then, later, you can use that name like a command — and Python runs the chunk every time you call it.
Here’s the syntax:
def draw_square():
for i in range(4):
t.forward(60)
t.right(90)
Let’s break it down:
def— the keyword that starts a function definition. (It’s short for “define.”)draw_square— the name you’re giving this function. You can call it almost anything, but the convention is lowercase with underscores, and the name should describe what it does.()— the parentheses are required (we’ll put things in them in a minute).:— the colon, just like aforloop.- The next lines are indented — pushed in by 4 spaces. Same rule as loops. The indented lines are the function’s body.
Notice: typing def draw_square(): doesn’t do anything. It
defines the function. The turtle hasn’t drawn anything yet. To
use the function, you have to call it:
draw_square()
That’s the call. The parentheses again — this time empty, because we’re not passing any information yet.
Now the full program:
import turtle
t = turtle.Turtle()
def draw_square():
for i in range(4):
t.forward(60)
t.right(90)
draw_square()
Save. Run. The turtle draws a square.
So far this is just one square — the same as before. But watch what happens when we want three:
import turtle
t = turtle.Turtle()
def draw_square():
for i in range(4):
t.forward(60)
t.right(90)
draw_square()
t.penup()
t.goto(-100, 0)
t.pendown()
draw_square()
t.penup()
t.goto(100, 0)
t.pendown()
draw_square()
Three squares, but only one copy of the actual square-drawing
code. The function draw_square() got called three times. If we
ever wanted to change how the squares are drawn — say, to use a
different color or a thicker pen — we’d change it in one place,
not three.
This is one of the most powerful ideas in all of programming: write something once, use it many times.
Functions with parameters
Right now draw_square() always draws a square of size 60. What
if we want different sizes?
We can give the function a parameter — a piece of information the function needs in order to do its job. For us, the size:
def draw_square(size):
for i in range(4):
t.forward(size)
t.right(90)
Look at the differences:
def draw_square(size):— the parametersizegoes in the parentheses.t.forward(size)— instead of always 60, we use whateversizewas passed in.
To call it, you put the size in the parentheses:
draw_square(50)
draw_square(100)
draw_square(75)
Each call draws a different-sized square — the same function,
but with different values for size.
Build the full program and try it:
import turtle
t = turtle.Turtle()
def draw_square(size):
for i in range(4):
t.forward(size)
t.right(90)
draw_square(40)
t.penup()
t.goto(-100, 0)
t.pendown()
draw_square(80)
t.penup()
t.goto(100, 0)
t.pendown()
draw_square(60)
Three squares, three different sizes, all drawn by the same function.
Checkpoint: You’ve defined a function that takes a parameter and called it more than once with different values. This is the natural stop point if class is cut short.
Part B: Build a neighborhood
Now we’ll do something fun: build a row of houses, each one drawn by a function.
A function for the house
A house is just a square (the body) with a triangle on top (the roof). Build this in your file:
def draw_house(size):
# the body — a square
for i in range(4):
t.forward(size)
t.left(90)
# move up to the top of the square
t.left(90)
t.forward(size)
t.right(90)
# the roof — a triangle
for i in range(3):
t.forward(size)
t.left(120)
# come back down
t.right(90)
t.forward(size)
t.left(90)
A few things to notice:
- This function is longer than
draw_square. Functions can be as long as they need to be. - Inside it, we use two for loops (one for the square, one for the triangle). Functions can have all the same constructs any other Python code can.
- The
t.left(90) / t.forward(size) / t.right(90)block in the middle is moving the turtle to where the triangle should start, without changing the overall direction it’s facing. Spatial reasoning is part of functions like this. - The lines starting with
#are comments — explaining what each section does. Useful in longer functions.
Build the row
Now use draw_house() to make a row of three houses:
import turtle
t = turtle.Turtle()
def draw_house(size):
# ... (the function from above)
# draw three houses in a row
t.penup()
t.goto(-200, 0)
t.pendown()
draw_house(60)
t.penup()
t.goto(-50, 0)
t.pendown()
draw_house(60)
t.penup()
t.goto(100, 0)
t.pendown()
draw_house(60)
Save. Run. Three houses across the bottom of the stage.
This is the base goal. Three houses, drawn by your own function, called three times.
Stretch — different sizes and colors
Modify draw_house to also take a color:
def draw_house(size, color):
t.color(color)
# ... (the rest of the function)
Now you can have houses of different sizes and colors:
draw_house(50, "red")
# move
draw_house(80, "blue")
# move
draw_house(60, "green")
A function can take as many parameters as you want, separated by
commas. Both inside the def and inside the call, the order
matters — size is first, color is second, every time.
Extension — functions calling functions
Build smaller helper functions and have draw_house use them:
def draw_square(size):
for i in range(4):
t.forward(size)
t.left(90)
def draw_triangle(size):
for i in range(3):
t.forward(size)
t.left(120)
def draw_house(size):
draw_square(size)
# move to top of square
t.left(90)
t.forward(size)
t.right(90)
draw_triangle(size)
# come back down
t.right(90)
t.forward(size)
t.left(90)
Now draw_house is much shorter — it just calls the smaller
functions. And draw_square and draw_triangle could be used
on their own, too. This is how big programs get built: lots of
small functions, each doing one thing well, calling each other.
Try it. Then build an even bigger function — draw_neighborhood()
— that calls draw_house() three times with different positions
and colors. Now you have:
draw_square()anddraw_triangle()— small building blocksdraw_house()— uses the small onesdraw_neighborhood()— usesdraw_house()three times
Three layers of functions, each one calling the layer below it. Real programs are built like this.
Wrap-up
Before we leave, share with the room:
- What did you build with functions today?
- Did anyone try the functions-calling-functions extension?
- If you wanted to add a sun and moon to your scene, would you
write
draw_sun()anddraw_moon()functions or copy code? (The right answer is: it depends on whether you want to draw more than one of them.)
You learned the single biggest idea in writing reusable code today. Once you can write functions, you can build programs out of building blocks instead of typing everything from scratch. Functions show up in every programming language — Python, the JavaScript you’ll learn in Phase 7, even some of the spreadsheet formulas your parents probably use at work. The syntax varies; the idea is universal.
You also met parameters — the way functions take in information. Parameters are how you make one function work for many situations.
If you missed this session
Open Thonny and start a new file. Save it as functions.py.
Then:
-
Build the basic
draw_square()function:import turtle t = turtle.Turtle() def draw_square(): for i in range(4): t.forward(60) t.right(90) draw_square()Save, run. A square.
-
Modify it to take a size parameter, and call it multiple times with different sizes (with
penup/gotobetween). -
Build the
draw_house(size)function from Part B and call it three times to make a row of houses.
About 40 minutes of work. Pay attention to indentation and to
the parentheses (one set in the def, one set when you call).
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Default values: write a function as
def draw_square(size=50):and you can call it as eitherdraw_square()(uses 50) ordraw_square(80)(uses 80). Useful for making functions easier to call. - A function that draws a polygon of any number of sides:
def draw_polygon(sides, length):usingrange(sides)and360 / sidesas the turn angle. - A function that takes a color list and draws a row of squares
in those colors: pass
["red", "blue", "green"]as a parameter and loop through it. (Lists are coming in a later phase, but curious students can try.)
What’s next
Next week we’ll learn about variables — how to give names to numbers and use them throughout your code. Combined with what you know now, variables let you build programs that change behavior based on values you set. The neighborhood gets even better.
Session 5: Variables and a little math
Session 5: Variables and a little math
Phase 2 — Python with Turtle · Session 5 of 8
What we’re learning today
You’ve actually been using variables since Session 3 (the i in
for i in range(N):) and Session 4 (parameters in your
functions). Today we’ll name our own variables, do math with
them, and use them to make drawings that grow, shift, and change.
By the end of class, you’ll have built a “tower of squares” that
gets bigger as it goes up — driven entirely by a variable.
You’ll need to remember from last time
- Functions —
def name(parameter):to define,name(value)to call. The body is indented underneath. - Loops —
for i in range(N):runs the indented body N times. - The peanut butter rule is still in effect.
- Variables in Scratch — the orange “Make a Variable” blocks,
set score to 0andchange score by 1. Same idea today, different syntax.
Part A: Names for values
Open Thonny and start a new file. Save it as variables.py.
Make a variable
In Scratch, you made a variable by clicking “Make a Variable.” In Python, you make a variable by writing its name and giving it a value:
size = 50
That’s it. The variable size now holds the value 50. No
button to click; no setup required. Just write the name and
assign with =.
The = sign in Python isn’t “equals” the way it is in math —
it’s “give the name on the left this value on the right.” Some
programmers read size = 50 out loud as “size gets fifty.”
That helps remember it’s an action, not a fact.
Use the variable in turtle code:
import turtle
t = turtle.Turtle()
size = 50
t.forward(size)
t.right(90)
t.forward(size)
t.right(90)
t.forward(size)
t.right(90)
t.forward(size)
t.right(90)
Save. Run. A square of size 50.
Now change the variable to size = 100. Save. Run. A square of
size 100.
This is the power of variables: you change one number at the top, and the entire program changes behavior. Imagine if your square’s size appeared in 20 different places in your code — without variables you’d have to find and change every one.
Math with variables
Variables aren’t just storage — they’re values, so you can do math on them. Python knows the basic arithmetic operators:
| Operator | Means | Example |
|---|---|---|
+ | add | 5 + 3 is 8 |
- | subtract | 5 - 3 is 2 |
* | multiply | 5 * 3 is 15 |
/ | divide | 15 / 3 is 5.0 |
You can use them with variables too:
size = 50
big_size = size * 2 # 100
small_size = size / 2 # 25.0
size_plus_ten = size + 10 # 60
Variables can hold the result of math. They can also use other variables in their math.
Changing a variable
You can change what a variable holds. Just assign a new value:
size = 50
print(size) # prints 50
size = 100
print(size) # prints 100
The most common pattern: change a variable based on its current value:
size = 50
size = size + 10 # now size is 60
size = size + 10 # now size is 70
This is the Python equivalent of Scratch’s change size by 10.
Read it as: “size gets the current size plus 10.”
(There’s a shorter way to write it: size += 10 does the same
thing as size = size + 10. Either is fine. Use whichever you
find clearer.)
Using a variable inside a loop
Now we put it together. Add this to your file:
import turtle
t = turtle.Turtle()
size = 30
for i in range(5):
# draw a square of the current size
for j in range(4):
t.forward(size)
t.right(90)
# move up
t.penup()
t.right(90)
t.forward(size)
t.left(90)
t.pendown()
# grow the size for next time
size = size + 15
Save. Run.
A tower of squares! Each one is bigger than the last because
size increases by 15 each iteration of the outer loop.
The first square is 30. Then size becomes 45 — second square is 45. Then 60. Then 75. Then 90.
The variable lives across iterations. The loop and the variable work together to produce the growing pattern.
Checkpoint: You’ve built a drawing that uses a variable that changes inside a loop, and the visual output reflects the changing variable. This is the natural stop point if class is cut short.
Part B: A variable-driven drawing
Now build something more complex with variables.
Base goal — a fan of lines
Build a fan of 12 lines, all coming from the same starting point, each rotated a bit further than the last. The angle is the variable that changes:
import turtle
t = turtle.Turtle()
t.speed(0)
angle = 0
for i in range(12):
t.setheading(angle)
t.forward(100)
# come back to the start
t.penup()
t.backward(100)
t.pendown()
# rotate for next line
angle = angle + 30
Save. Run. A fan of 12 lines spreading out in a half-circle.
Notice: setheading(angle) uses the variable to point the turtle
in a specific direction each time. After drawing the line, we go
back to the center, then increase angle by 30 for the next one.
12 lines × 30 degrees = 360, so they go all the way around.
Stretch — multi-color tower
Combine variables and color. Build a tower (like in Part A) but have the color change with each iteration:
import turtle
t = turtle.Turtle()
t.speed(0)
size = 30
red_amount = 50
for i in range(8):
# use the variables to set color and draw
t.color((red_amount / 255, 0.5, 0.5))
for j in range(4):
t.forward(size)
t.right(90)
# move up
t.penup()
t.right(90)
t.forward(size)
t.left(90)
t.pendown()
# update variables
size = size + 10
red_amount = red_amount + 25
The t.color((red_amount / 255, 0.5, 0.5)) line uses an RGB
color (red, green, blue, each from 0 to 1). As red_amount grows,
the red part of the color gets brighter — so each square in the
tower has a slightly redder color than the one below it.
Don’t worry about the RGB syntax for now — just notice that the color changes because the variable does.
If RGB feels too much, simpler version: change pensize based
on i:
t.pensize(i + 1)
The first square uses pensize 1, the next 2, etc. Visible difference, simpler code.
Extension — a function
Wrap the tower in a function:
def draw_tower(start_size, count, growth):
size = start_size
for i in range(count):
for j in range(4):
t.forward(size)
t.right(90)
t.penup()
t.right(90)
t.forward(size)
t.left(90)
t.pendown()
size = size + growth
draw_tower(20, 5, 10)
# move
t.penup()
t.forward(150)
t.pendown()
draw_tower(40, 3, 20)
Now you have a flexible tower-drawing function. You pass in the starting size, how many squares, and how much each one grows. Two different towers, drawn with one function. This combines Sessions 4 + 5 — functions with parameters, plus variables that change inside the function.
That’s how real programs get built.
Wrap-up
Before we leave, share with the room:
- What’s the highest size your tower got to?
- Did anyone try the multi-color version?
- For the kids who built the function: did you draw multiple towers? What were the parameter values?
You learned today how to store information in named places and do math with it. That’s the foundation of every meaningful program — programs are mostly variables changing over time, with loops and conditionals deciding when and how.
Variables also gave you a new debugging tool: print(size) shows
you the current value of size in the shell. When something’s
not behaving the way you expect, sprinkling some print() calls
around your code is the most common way programmers figure out
what’s actually happening.
If you missed this session
Open Thonny and start a new file. Save it as variables.py.
Then:
-
Make a variable:
size = 50. Build a square that usest.forward(size)and run it. Changesizeto100, run again. Same code, different square. -
Practice changing variables:
size = size + 10. Try printing it after each change withprint(size). -
Build the tower from Part A — a
forloop that draws a square of sizesizeeach iteration, then increasessizeby 15.
About 30-40 minutes. Watch your indentation; the inner square loop is indented twice (once for the outer loop, once for being the inner loop’s body).
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Try negative growth:
size = size - 10. Tower gets smaller going up. (When does it become invisible?) - Use multiple variables in one drawing —
xandyfor position,sizefor shape. A tower that drifts sideways as it grows. - Try
print(size)inside the loop to watch the variable change in real time. The shell at the bottom of Thonny will show each value. - Use variables for the loop counter math:
for i in range(10):thent.forward(i * 5)makes a spiral that grows linearly. Combine witht.right(15)for the same spiral as Session 3.
What’s next
Next week: conditionals. We’ll bring back the if block,
this time in Python. Combined with variables, conditionals let
your programs decide what to do based on the values they’re
working with. After that, we have one more session of “putting
it all together” before the milestone project.
Session 6: Conditionals — if in Python
Session 6: Conditionals — if in Python
Phase 2 — Python with Turtle · Session 6 of 8
What we’re learning today
Today’s the last “new syntax” session of Phase 2. We bring back
the if block — but in Python instead of Scratch — and combine
it with everything else you know to make programs that decide
what to do based on the situation. Combined with variables, you
can build programs that change behavior based on values you set.
By the end of class, you’ll have a function that draws colored
shapes where the color depends on the size.
You’ll need to remember from last time
- Variables —
size = 50,size = size + 10. - Math operators —
+,-,*,/. - Functions with parameters —
def draw_square(size):. - The
ifblock from Scratch — “if something, then do this.” - Indentation matters — lines inside something are indented.
Part A: Making decisions in Python
Open Thonny and start a new file. Save it as decisions.py.
The if statement
Just like in Scratch, the if statement does something only
when a condition is true. In Python, it looks like this:
size = 70
if size > 50:
print("That's a big size!")
Walk through:
if— the keyword that starts the conditional.size > 50— the condition. This is a question Python can answer with True or False. Issizegreater than 50? Yes or no?:— the colon (required, just likeforanddef).- The next line is indented — it’s the body that runs only if the condition is true.
Run it. Since size is 70, and 70 is greater than 50, Python
prints “That’s a big size!”
Now change size = 70 to size = 30. Run again.
This time nothing prints. The condition 30 > 50 is false, so
Python skips the body of the if.
else for the other case
Often you want one thing to happen if the condition is true, and
something else to happen if it’s false. Use else:
size = 30
if size > 50:
print("Big!")
else:
print("Small!")
If size > 50, prints “Big!” Otherwise, prints “Small!”
else doesn’t need a condition — it just runs whenever the
if’s condition was false.
The colon and indentation rules are the same. The body of else
is indented underneath, just like if.
Comparison operators
The condition uses comparison operators to compare values. Python knows these:
| Operator | Means | Example |
|---|---|---|
> | greater than | 5 > 3 is True |
< | less than | 5 < 3 is False |
>= | greater than or equal to | 5 >= 5 is True |
<= | less than or equal to | 5 <= 4 is False |
== | equal to | 5 == 5 is True |
!= | not equal to | 5 != 3 is True |
The two important ones to be careful about:
==for “is equal to” — two equals signs, not one. One equals sign (=) means assignment (giving a variable a value). Two equals signs (==) means comparison (asking if two things are equal). They look the same and they’re not.!=for “is not equal to” — the exclamation point means “not.”
Try a few:
x = 10
if x == 10:
print("x is exactly 10")
if x != 5:
print("x is not 5")
if x >= 10:
print("x is at least 10")
Run. All three prints happen, because x is 10.
Now change x = 5. Only the second prints (5 != 5 is false,
so the third skips; 5 == 10 is false, so the first skips; but
5 != 5… actually that’s also false. Wait — let me re-read.
With x = 5: 5 != 5 is false. So… nothing prints. Try it.)
If you read those three carefully and predicted what would happen before you ran it — congratulations, you’re starting to think like a programmer.
elif for multiple cases
Sometimes you want to check several conditions in order. elif
(short for “else if”) chains them together:
size = 30
if size > 100:
print("Huge!")
elif size > 50:
print("Big!")
elif size > 20:
print("Medium!")
else:
print("Tiny!")
Python checks each condition in order. The first one that’s
true, its body runs. If none are true, the else body runs.
With size = 30: 30 > 100 is false, 30 > 50 is false, 30 > 20
is true → prints “Medium!” The remaining elifs and else are
skipped.
You can have as many elif lines as you want. The else at
the end is optional.
Checkpoint: You’ve built an if/else (or if/elif/else)
that makes decisions based on the value of a variable. This is
the natural stop point if class is cut short.
Part B: Decisions in your drawings
Now we use conditionals where they really shine — making your drawings react to the values they’re working with.
Base goal — colored squares by size
Build a function that draws a square, but colors it red if big and blue if small:
import turtle
t = turtle.Turtle()
t.speed(0)
def draw_colored_square(size):
if size > 60:
t.color("red")
else:
t.color("blue")
for i in range(4):
t.forward(size)
t.right(90)
# draw a few squares of different sizes
draw_colored_square(40)
t.penup()
t.forward(60)
t.pendown()
draw_colored_square(80)
t.penup()
t.forward(100)
t.pendown()
draw_colored_square(50)
Save. Run.
You should see three squares: small blue, big red, medium blue. The function decides the color based on the size each time it’s called.
This is small but powerful. You’ve combined functions (Session 4), variables (Session 5), and conditionals (today) into one little decision-making drawing function.
Stretch — three categories with elif
Add a third size category. Use elif:
def draw_colored_square(size):
if size > 80:
t.color("red")
elif size > 40:
t.color("orange")
else:
t.color("blue")
for i in range(4):
t.forward(size)
t.right(90)
Now squares are red, orange, or blue depending on size. Try calling with various sizes (20, 50, 90, etc.) and see the colors.
Extension — a row of growing squares with conditional colors
Combine all of Phase 2 into one program: a loop that draws a growing row of squares, colored by size:
import turtle
t = turtle.Turtle()
t.speed(0)
def draw_colored_square(size):
if size > 80:
t.color("red")
elif size > 40:
t.color("orange")
else:
t.color("blue")
for i in range(4):
t.forward(size)
t.right(90)
size = 10
for i in range(12):
draw_colored_square(size)
# move to next position
t.penup()
t.forward(size + 15)
t.pendown()
# grow
size = size + 8
Save. Run.
A row of 12 squares, growing from small (size 10) to large (size 98). The colors transition: blue, blue, blue, blue, orange, orange, orange, orange, orange, red, red, red. The conditional inside the function picks the color; the loop drives the size forward.
This program uses every concept from Phase 2 so far: typing, sequences, loops, functions with parameters, variables, arithmetic, AND conditionals — all in one ~25-line program. You just built that.
Wrap-up
Before we leave, share with the room:
- What did your conditional decide on?
- For the kids who built the row of growing squares — does the color transition feel right, or would you tweak the thresholds?
- Did anyone try a condition with
andoror? (Hint: those let you combine multiple conditions, likeif size > 20 and size < 50:. We didn’t formally cover them, but they work.)
You learned today how to make programs that decide — based on values, based on variables, based on what’s happening. Combined with everything else, you now have the full toolbox that powers most of what computers do: store information (variables), repeat actions (loops), respond to inputs (events, which we’ll see again in Pygame), bundle code (functions), and make decisions (conditionals).
That’s basically what programming is. The rest is mostly applying these building blocks to specific problems.
If you missed this session
Open Thonny and start a new file. Save it as decisions.py.
Then:
-
Build the basic
if/else:size = 30 if size > 50: print("Big!") else: print("Small!")Run. Then change
size = 70and run again. Different output. -
Try
elifwith three categories — small, medium, large. -
Build the
draw_colored_square(size)function from Part B above. Call it three times with different sizes.
About 30 minutes. Watch indentation — the body of if, elif,
and else is indented underneath each one.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Combine conditions with
and:if size > 20 and size < 50:. Both conditions must be true. - Combine with
or:if size < 10 or size > 100:. Either condition can be true. - Use
not:if not size > 50:is the same asif size <= 50:. Sometimes one reads better than the other. - Conditional inside a loop, deciding what to draw each
iteration:
Make a row that’s half triangles, half squares.for i in range(8): if i < 4: # draw a triangle pass else: # draw a square pass
What’s next
Next week we put it all together — sequences, loops, functions, variables, conditionals — and build a creative drawing project that uses all of it. (Phase 1 also had events; Turtle doesn’t really do events, so we save the event stuff for Phase 6 when we start writing real games in Pygame.) After next week is the milestone session, where you’ll plan and build your own drawing project.
Session 7: Putting it together — a garden scene
Session 7: Putting it together — a garden scene
Phase 2 — Python with Turtle · Session 7 of 8
What we’re learning today
You now know everything in Phase 2: typing, turtle commands, loops, functions, variables, conditionals. Today we’ll combine all of it into one creative drawing — a garden of flowers — where each flower is drawn by a function, multiple flowers are drawn by a loop, and the colors and sizes vary based on conditionals. By the end of class, you’ll have a complete scene that’s substantially more sophisticated than anything in Phase 1.
You’ll need to remember from last time
Everything. No, really:
- Functions with parameters —
def name(parameter): - Variables and arithmetic —
size = 50,size = size + 10 - For loops —
for i in range(N): - Conditionals —
if/elif/else - All your turtle commands — forward, right, left, color, pensize, penup, pendown, goto, setheading, circle, write
This is the test: can you put it all together? (Spoiler: yes.)
Part A: Build a flower function
Open Thonny and start a new file. Save it as garden.py.
Plan the flower
Before any code, think about what a flower looks like in Turtle terms. We need:
- A stem — a vertical line going up from the bottom.
- Petals at the top — several circles arranged in a ring.
- A position so we can put it somewhere on the stage.
- A color so different flowers can look different.
- A size so big and small flowers can coexist.
Position, color, size — those are parameters. The stem and petals are what the function does.
Build it
import turtle
t = turtle.Turtle()
t.speed(0)
def draw_flower(x, color, petal_size):
# move to the bottom of the stem
t.penup()
t.goto(x, -100)
t.pendown()
# draw the stem
t.color("green")
t.pensize(3)
t.setheading(90) # face up
t.forward(80)
# draw the petals (8 circles in a ring)
t.color(color)
t.pensize(2)
for i in range(8):
t.circle(petal_size)
t.right(45)
# draw one flower
draw_flower(0, "red", 15)
Save. Run.
A red flower in the middle of the stage. Stem going up, eight red circles around the top.
Walk through what’s happening:
draw_flowertakes three parameters:x(where on the stage),color(petal color),petal_size(how big each petal circle is).- The stem is always drawn green and 80 pixels tall.
- The petals loop runs 8 times. Each iteration draws a circle, then turns 45 degrees so the next circle starts in a different position. 8 × 45 = 360, so they go all the way around.
Try it with different parameters
Add three more flower calls:
draw_flower(-150, "yellow", 20)
draw_flower(150, "purple", 12)
draw_flower(-50, "pink", 18)
Run. Now you have four flowers across the stage, each different.
This is the same function called four times with four different sets of arguments. One function, multiple flowers. You’ve seen this pattern before in Session 4 — the power is the same; the visual is fancier.
Checkpoint: You have a draw_flower function and at least
three flowers on the stage drawn by calling it. This is the
natural stop point if class is cut short.
Part B: A garden with variation
Now we use loops + conditionals to make a bigger garden where the flowers vary in interesting ways.
Base goal — a loop-driven row
Use a for loop to draw a row of flowers across the stage. Pick
a starting position, increment the x-coordinate each time:
import turtle
t = turtle.Turtle()
t.speed(0)
def draw_flower(x, color, petal_size):
t.penup()
t.goto(x, -100)
t.pendown()
t.color("green")
t.pensize(3)
t.setheading(90)
t.forward(80)
t.color(color)
t.pensize(2)
for i in range(8):
t.circle(petal_size)
t.right(45)
# draw a row of 5 flowers
x = -200
for i in range(5):
draw_flower(x, "red", 15)
x = x + 100
Run. Five red flowers in a row.
The variable x starts at -200 and increases by 100 each
iteration. The loop runs 5 times; the x value drives where each
flower appears.
Stretch — different colors per flower
Right now all five are red. Let’s make each one a different color, using a conditional inside the loop to pick the color based on the iteration number:
x = -200
for i in range(5):
if i == 0:
color = "red"
elif i == 1:
color = "orange"
elif i == 2:
color = "yellow"
elif i == 3:
color = "purple"
else:
color = "pink"
draw_flower(x, color, 15)
x = x + 100
Now you have a rainbow of flowers. The conditional picks a color
based on i, then draw_flower uses it.
(Yes, this is verbose. There’s a better way using a list — but we haven’t covered lists yet. We’ll get there in Phase 3.)
Extension — a complete scene
Add more elements to make a real garden scene:
- A sun in the upper corner.
- Grass at the bottom (a horizontal green line).
- Sky by changing the background color.
- A title at the top using
t.write(). - Variable petal sizes — make some flowers bigger than others using another conditional or by varying the parameter.
Here’s a starting structure:
import turtle
t = turtle.Turtle()
t.speed(0)
# set the scene
t.bgcolor("skyblue")
def draw_flower(x, color, petal_size):
# ... (same as before)
def draw_sun(x, y, size):
t.penup()
t.goto(x, y)
t.pendown()
t.color("yellow")
t.pensize(2)
t.circle(size)
def draw_grass(y):
t.penup()
t.goto(-240, y)
t.pendown()
t.color("green")
t.pensize(8)
t.forward(480)
# build the scene
draw_sun(180, 120, 40)
draw_grass(-100)
# row of flowers (with the conditional color picker from above)
x = -200
for i in range(5):
if i == 0:
color = "red"
elif i == 1:
color = "orange"
elif i == 2:
color = "yellow"
elif i == 3:
color = "purple"
else:
color = "pink"
draw_flower(x, color, 15)
x = x + 100
# title
t.penup()
t.goto(-100, 150)
t.pendown()
t.color("black")
t.write("My Garden", font=("Arial", 24, "normal"))
Save. Run.
A complete garden scene: blue sky, yellow sun, green grass, five colorful flowers in a row, and a title.
This program uses every concept from Phase 2: typing the code
in Thonny, turtle commands, multiple sequences (each function is
a sequence), three loops (the petals, the for-loop driving the
row, plus you could argue more), three functions with parameters,
two variables (x and color) that change inside the loop,
and a conditional choosing the color.
You went from “just typed your first line of Python” to “writing a fifty-line program with multiple functions and a creative output” in seven weeks. That’s a real arc.
Wrap-up
Before we leave, share with the room:
- What did your garden look like? Anyone do something unexpected?
- For the kids who built the full scene — was anything trickier than you expected?
- What would you do differently if you started over?
Important: start thinking about your milestone project for next week. You’ll have one session — next week — to plan and build a Turtle drawing of your own design, then show it to the class. Some seed ideas:
- A landscape scene — mountains, trees, a river, a sunset.
- A fancy pattern — like a snowflake or a mandala (radial symmetry around a center point).
- An abstract drawing — colored shapes arranged for visual effect.
- A scene from somewhere you like — a beach, your room, a page from a comic.
- An animal built from circles and shapes.
- Your name in giant decorated letters.
Don’t decide today. But come to next week’s class with at least one idea you’re excited about.
If you missed this session
Open Thonny and start a new file. Save it as garden.py. Then:
-
Build the
draw_flower(x, color, petal_size)function from Part A. Call it once to draw a single flower. -
Call it three or four more times with different positions, colors, and sizes.
-
Wrap the calls in a
forloop driven by anxvariable that starts at -200 and increases by 100 each iteration. -
Add a conditional inside the loop to pick a different color for each flower (Part B stretch).
About 40-50 minutes. Pay attention to indentation — the loop
body is indented once, the if/elif/else bodies inside the loop
are indented twice.
If you get stuck, ask your buddy at the start of next class. And think about what you want to build for your own milestone project.
Stretch and extension ideas
- Replace the
if/elif/elsecolor-picker with a list. (We haven’t formally taught lists, but a curious student can try:colors = ["red", "orange", "yellow", "purple", "pink"]and thencolor = colors[i]inside the loop. That replaces five if/elif lines with one.) - Add variation in petal count. Some flowers have 8 petals; some have 6 or 12. Use a parameter for it.
- Build a 2D grid of flowers using a nested for loop (outer loop varies y, inner loop varies x). A whole field.
- Add clouds to the sky using ovals (or just multiple circles side by side).
What’s next
Next week is your milestone project + demo day. You’ll plan, build, and demo your own Turtle drawing — your design, your choices. Bring an idea (or two). After Phase 2, we move into Phase 3 — Python basics, where Python becomes more general: no more turtle, but lots more of what Python can do.
Session 8: Milestone project + demo day
Session 8: Milestone project + demo day
Phase 2 — Python with Turtle · Session 8 of 8
What we’re learning today
Today is your day. You’ll plan, build, and demo a Turtle drawing of your own design — your idea, your code, your scene. By the end of class, you’ll have shown your work to the room and finished Phase 2. There’s no new programming concept this week; today is about putting your skills to use on something you chose.
You’ll need to remember from last time
- Everything from Sessions 1-7. Functions, loops, variables, conditionals, all the turtle commands.
- The garden scene you built last week — proof you can make something substantial.
- The idea you (hopefully) brought with you today. (If not, Part A has a list to start from.)
Part A: Plan and start building
Today is structured tighter than Phase 1’s milestone — we have one session for planning, building, and demoing — so we move quickly and you decide what’s important.
The plan
Take a piece of paper or open a blank text file. Answer these four questions about your project. Keep it short — first answers are usually right:
-
What are you drawing? (One sentence. Examples: “A snowy mountain landscape.” “My initials in giant decorated letters.” “A snowflake pattern.”)
-
What functions do you need? (Each function does one thing. Examples:
draw_mountain(),draw_tree(),draw_letter_A(),draw_petal(). Brainstorm 2-5.) -
What will vary? (What are the parameters? Position? Color? Size? List which functions need which parameters.)
-
What’s the simplest version that’s still cool? (If you only had 30 minutes to build, what would you do FIRST? What would you add LATER if there’s time? This is the most important question.)
If you don’t have an idea
Pick one and modify it slightly:
- A snowy mountain landscape — triangles for mountains, small circles for snow, a cabin or tree at the bottom.
- A snowflake — radial symmetry around a center point. Use a function to draw one “arm” and a loop to call it 6 times rotated.
- A name in giant letters — each letter is a function,
drawn with
forward/right/left/ pen control. - A field of flowers like last week, but bigger or with different flower types.
- An abstract pattern — shapes radiating from a center, spirals within spirals, anything geometric.
- A simple animal — a cat, a fish, a bird built from circles and lines.
- Your favorite emoji — recreated in turtle code. You’d be surprised what’s possible.
Pick one. Spend two minutes deciding. Don’t agonize — you can modify the idea while you build.
Show Mr. Eric
When your plan is ready (about 10 minutes in), show it to Mr. Eric. He’ll either say “go build it” or ask one question that helps you tighten it up. Then start building.
Start building — the simplest version first
Look at your answer to question 4. Build that first. Get the simplest version working before adding anything fancy. Real programmers do this — get something runnable as fast as possible, then improve from there.
If you said “the simplest version is just one mountain” — build one mountain. Run it. See it. Then add a second one. Then snow. Then trees.
Don’t try to type the whole project before testing. Build small. Test often. Add piece by piece.
When you get stuck
The same five steps as always:
- Read your code carefully. Did you tell the computer the right thing?
- Read the error message. Most Python errors point at the exact line.
- Look at how you did something similar before. Last week’s garden? Open it for reference.
- Ask your buddy.
- Ask Mr. Eric.
The Getting unstuck appendix has more on this.
Checkpoint: You have a written plan with the four questions answered, Mr. Eric has signed off, and you’ve started building the simplest version. This is the natural stop point if class is cut very short — but for today, we’re going to keep going to the demo half.
Part B: Final polish, then demo
After the break, you have about 15 minutes to finish or polish, then we demo.
Polish (~10-15 min)
Be honest with yourself about what’s done. Three things make a project ready to demo:
- It runs. You can press the green flag and see something without an error.
- It does what your plan said it would do — at least the simplest version (your answer to question 4).
- You can explain how it works. Walk someone through the functions and what they each draw.
If your project doesn’t meet those three, focus on getting it there before adding anything new. A working simple project is way better than a broken ambitious one.
Buddy test (~5 min)
Swap with your buddy. They run your project; you run theirs. Notice:
- Did it run?
- Did the visual surprise them in any way (good or bad)?
- Could they tell what each part of the drawing was?
Tell each other one thing you noticed.
Demos
Each of you demos for 3-5 minutes. Same format as Phase 1 demo day:
- Show your drawing. Run it. Let the class see it appear.
- Tell us about it. What were you trying to make? What functions did you write?
- Tell us one thing that was hard. Honest, not bragging. Maybe the math on a turn angle. Maybe positioning a sprite correctly. Things that broke and how you fixed them.
- Take one question. Someone asks something specific — “how did you make the snowflake?”, “what made you pick those colors?” Answer as best you can.
When it’s not your turn, watch carefully and have one specific question ready when called on. “I really liked X” is nice; “How did you make X?” is better.
After everyone demos
Mr. Eric will say a few specific things about each of your projects. Then we’re done — Phase 2 is complete.
What you accomplished
- You built two programs from scratch in Python this phase: the garden last week, and your own project today. That’s two more than you’d built before Phase 2.
- You learned functions — the most powerful tool for organizing code that we cover in the entire curriculum.
- You typed real Python code, made (and fixed) lots of error messages, and you kept going. That’s what programming is.
- You shipped two milestone projects so far in your programming journey. The Phase 1 game; today’s drawing. Most adults haven’t shipped one creative project ever. You’re at two.
What’s next
Next week starts Phase 3 — Python basics. Real Python beyond the turtle.
What’s different in Phase 3:
- No more turtle. The turtle was great as a bridge — visual, immediate. But real Python programs do all sorts of things that aren’t drawing. We’ll write programs that think with text, numbers, lists of things.
- Strings, lists, files. Programs that talk to you, ask questions, remember things across runs.
- Real-world projects. A number-guessing game. A text adventure. Something that reads a file (your favorite Bible verses, maybe?) and does something useful with it.
- The Thonny debugger. The single most useful Python tool you haven’t met yet. It lets you watch your program run line by line and see exactly what’s happening.
Bring your machine and your brain. We’re going deeper.
If you missed this session
Two cases:
If you missed only the demo half: Show your project to Mr. Eric at the start of next week’s class. He’ll give you the same kind of feedback the demo would have.
If you missed the whole session and don’t have a finished project: No big deal. Pick one of the seed ideas above (or extend last week’s garden), build it at home using everything you know, and bring it next week. Or: just join us in Phase 3 — what you’ve already built is plenty to be proud of.
Stretch and extension ideas
If you have time after the demos and want to keep going:
- Polish your project more. Add details, sounds, more variation.
- Build a second project of a different style. With everything you know now, the second one is faster than the first.
- Show your work to your family. They will be impressed.
- Save your project files. You’ll want to look back at them in a year and be amazed at what you knew at this stage.
What’s next
Phase 3 — Python without the turtle. See you there.
Phase 3 — Python basics
Phase 2 used the turtle to make typing Python feel friendly and visual. This phase is where Python becomes general-purpose — the language used by professionals to write everything from scripts that automate work, to data analysis, to web servers, to AI tools.
What this phase is
The biggest phase in the curriculum. Sixteen sessions on the core building blocks of Python: text, numbers, decisions, loops, functions, lists, dictionaries, files, and error handling. By the end, you’ll be able to write small programs that talk to people, store information, read and write files, and handle mistakes gracefully.
We leave the turtle behind. No more drawings on a stage. Instead, your programs run in the shell at the bottom of Thonny — they print things, ask the user questions, and respond based on what they’re told. Less visual, more flexible. Most software in the world works this way at its core.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to Python without turtle | print, input, the shell |
| 2 | Variables and types | int, float, string, type conversion |
| 3 | Strings | concatenation, methods, f-strings, indexing |
| 4 | Conditionals (deeper) | and, or, not; multi-condition logic |
| 5 | Loops + the Thonny debugger | while loops; the debugger formally |
| 6 | Functions + return values | return; thinking about scope |
| 7 | First real project — number-guessing game | First text-based game |
| 8 | Lists | the most important data structure in Python |
| 9 | More lists; iteration patterns | for over lists, len, append, slicing |
| 10 | Dictionaries | key-value lookups |
| 11 | Reading files | open, line iteration |
| 12 | Hangman | bigger project applying lots |
| 13 | Error handling | try / except |
| 14 | Putting it together — text adventure or CSV reader | Big integration project |
| 15 | Milestone project work day 1 | Plan + build |
| 16 | Milestone project work day 2 + demo | Polish + demo |
A lot of these sessions cover ideas you’ve already seen in Phase 1 (Scratch) or Phase 2 (Turtle) — variables, conditionals, loops, functions. What’s new is how Python does each one in a text-based context, plus several genuinely new tools: lists, dictionaries, files, the debugger, and proper error handling.
What you’ll build
Multiple working programs throughout the phase, not just one big project at the end:
- Session 1: a tiny program that asks for your name and greets you.
- Session 7: a number-guessing game. The computer thinks of a number; you have to guess it.
- Session 12: a hangman game. Real game logic, real word list, real win/lose conditions.
- Session 14: a text adventure or a CSV reader. Multi-room, multi-choice text-based game, OR a program that reads a real file (Bible verses, sports stats, whatever interests you) and does something with it.
- Session 16: a milestone project of your own design. Free choice within “make something using Python that does something useful or fun.”
What you’ll need
Same machine as Phase 2 — Thonny is already installed; nothing
new to install for Phase 3. The Python turtle library you used
in Phase 2 is replaced with Python’s built-in print, input,
file I/O, and other tools — all of which come standard with
Python.
If you want to work at home, the Installing on your own computer appendix has Thonny instructions.
How sessions work
Same shape as Phases 1 and 2:
- Part A introduces a new idea with a guided exercise. Stands alone if class gets cut short.
- Part B is open practice or a project that uses Part A. Often tiered (base / stretch / extension).
- Wrap-up — a few minutes of sharing what you did.
A note about the shell
Phase 2 had two windows: Thonny (where you typed code) and the turtle window (where drawings appeared). Phase 3 uses just Thonny. The shell at the bottom of the editor is where your program prints things and where you type answers when it asks.
The shell is also useful by itself — you can type one-line Python expressions there and press Enter to see what they do. We’ll use it for quick experiments throughout the phase.
A note about errors
Phase 2 had typos and syntax errors. Phase 3 will have more of them — partly because the syntax is more varied, partly because your programs are doing more. The Reading error messages appendix gets fleshed out as Phase 3 progresses.
The single best advice: read the error message slowly. Python’s errors are friendlier than they look once you get used to them.
Where to start
Session 1: Welcome to Python (without the turtle) opens with a callback to a moment from Session 1 of Phase 1. You might want to glance at the Phase 1 Welcome chapter before reading, just to remember.
When you’re stuck, the Getting unstuck appendix has the checklist. The Glossary explains vocabulary. New terms get added as Phase 3 introduces them.
Welcome to general Python. Let’s go.
Session 1: Welcome to Python (without the turtle)
Session 1: Welcome to Python (without the turtle)
Phase 3 — Python basics · Session 1 of 16
What we’re learning today
Phase 2 used the turtle to give your code something visual to
do. Phase 3 leaves the turtle behind. Today we’ll learn the two
most-used commands in all of Python: print (Python tells
you something) and input (Python asks you something). By
the end of class, you’ll have written a program that talks to
the person running it.
You’ll need to remember from last time
- Thonny. Open the editor, type code, save with
.py, click the green Run button. - Variables.
name = "Sam"puts the value"Sam"in a variable calledname. - The peanut butter rule. Still in effect. Maybe even more in effect today.
- You finished Phase 2. You wrote real Python code with functions, loops, conditionals, all of it. Today builds on that.
Part A: A familiar friend (and print)
You may notice Mr. Eric brought the bread back today. Loaf, peanut butter, jelly, knife, plate. The whole setup from Session 1 of Phase 1.
Same exercise: tell Mr. Eric how to make a peanut butter and jelly sandwich, step by step. He’ll do exactly what you say.
But notice: you’re way better at this now. Eighteen weeks ago, the room couldn’t even get past “open the peanut butter.” Today, the instructions are tighter, more careful. Some of you are already saying things like “grab the handle of the knife between your thumb and forefinger” without being prompted.
That’s not nothing. Programming has changed how you give instructions. You think about steps differently. You notice when something is ambiguous. That’s a skill you’ll have for the rest of your life.
Why we’re doing it again now
For all of Phase 2, the turtle was your safety net. If your code was wrong, the visual feedback told you immediately — “the square’s lopsided” or “the turtle went off-screen.” Today the turtle goes away. We’re using just text.
That means the peanut butter rule matters even more. With text, mistakes are quieter. A missing letter, a missing colon, a typo in a variable name — and your program either crashes or quietly does the wrong thing. There’s no visual to clue you in.
Today you start writing programs that talk to people. People expect their programs to work. That puts more pressure on you to be precise. Welcome to real programming.
Hello, world
Open Thonny. Start a new file. Save it as hello.py.
Type just one line:
print("Hello, world!")
Save. Run.
Look at the shell at the bottom of Thonny. You should see:
Hello, world!
That’s it. That’s your first non-turtle Python program.
What’s different from Phase 2:
- No
import turtle. We’re not using the turtle today. - No
t = turtle.Turtle(). No turtle. - Output goes to the shell at the bottom, not to a separate drawing window.
Try a few more print calls:
print("Hello, world!")
print("My name is", "your name here")
print("Five plus three is", 5 + 3)
print("Three tens is", 10 * 3)
Save. Run.
Each print call shows up on its own line in the shell.
Notice that print can take multiple things separated by
commas. Each thing gets printed with a space between. Numbers
don’t need quotes (they’re values); text needs quotes (it’s a
string).
“Writing ‘Hello, world!’ as your first program is a tradition. Every programming language ever has a ‘hello world’ program. You just joined that tradition.”
Checkpoint: You have a Python file that prints at least three different lines to the shell when you run it. This is the natural stop point if class is cut short.
Part B: Asking the user something
Now the other half — input. This is how a program asks the
user a question and gets their answer.
Your first interactive program
Replace your code with this:
name = input("What's your name? ")
print("Hello, " + name + "!")
Save. Run.
Watch the shell. You’ll see:
What's your name?
…and then nothing happens. The program is waiting for you.
Type your name and press Enter. Now:
What's your name? Sam
Hello, Sam!
Walk through what happened:
input("What's your name? ")— printed the prompt, waited for the user to type and press Enter, then returned what the user typed.name = input(...)— saved what the user typed in a variable calledname.print("Hello, " + name + "!")— used the variable in the output.
The + between strings is string concatenation — a fancy
word for “stick them together.” "Hello, " + "Sam" + "!" becomes
"Hello, Sam!". Like joining links in a chain.
Multi-question version
Try this — a longer version that asks multiple things:
name = input("What's your name? ")
color = input("What's your favorite color? ")
food = input("What's your favorite food? ")
print("Nice to meet you, " + name + "!")
print("Did you know that " + color + " " + food + " is delicious?")
Run it. Type three answers. The program echoes back a sentence about you.
Yes, the sentences are absurd. That’s the fun part.
Try this on your own
Pick one:
-
Base goal: Build a multi-question program that asks at least four things and uses all the answers in some output. Make the questions whatever you want — favorite Bible verse, favorite hymn, where you’d like to travel, what you ate for breakfast.
-
Stretch: Use a conditional (callback to Phase 2 Session 6!) to react to one of the answers:
color = input("What's your favorite color? ") if color == "blue": print("Great choice!") else: print("That's a fine color too, but blue is the best.")Try this for one or two of your questions.
-
Extension: Wrap a greeting in a function (callback to Phase 2 Session 4!):
def greet(name): print("Hello, " + name + "!") print("Welcome to Python.") your_name = input("What's your name? ") greet(your_name)Now you’ve combined input + functions + variables in one tiny program.
Wrap-up
Before we leave, share with the room:
- What questions did your program ask?
- Did anyone get a particularly weird sentence in their output?
- For the kids who did the conditional stretch — did the computer agree with your favorite color?
You learned today how to make programs talk to people. Every website that asks for a username, every game that asks “would you like to start a new game?”, every spreadsheet that prompts “are you sure you want to delete?” — they’re all doing what your program did today. Just at bigger scale, with prettier graphics. The core mechanic is the same: print a question, wait for input, do something with the answer.
The turtle was a fun way to learn. Text is how most software actually works under the hood.
If you missed this session
Open Thonny and start a new file. Save it as hello.py. Then:
-
Type
print("Hello, world!"). Save. Run. Look at the shell at the bottom — your message appears. -
Add a few more
printlines with different text or simple math. -
Now try
input:name = input("What's your name? ") print("Hi, " + name + "!")Save. Run. Type your name when prompted.
-
Build a multi-question version (the Part B base goal above).
About 30 minutes. If anything’s confusing, ask your buddy at the start of next class.
Stretch and extension ideas
- The user can also enter numbers, but
input()always returns a string even if they type a number. To do math with the number, you need to convert it:age = int(input("How old are you? ")). We’ll talk about why next week — try it for now. input()will also work with no prompt:answer = input()— but it’s confusing for the user (they don’t know what to type). Always pass a prompt.printwith no arguments —print()— outputs a blank line. Useful for spacing your output.
What’s next
Next week we formalize variables and types — what’s the
difference between 42, 42.0, and "42", and why does it
matter? (Spoiler: this is the answer to “why does input() give
me a string when I typed a number.”)
Session 2: Variables and types
Session 2: Variables and types
Phase 3 — Python basics · Session 2 of 16
What we’re learning today
You’ve been using variables for a while now (Phase 1 Session 6,
Phase 2 Session 5). Today we get specific about what kinds of
values variables can hold — numbers, decimals, text, and what
happens when you mix them up. By the end of class, you’ll have
written a program that does math with answers from the user, and
you’ll know why "5" + "3" is "53" instead of 8.
You’ll need to remember from last time
print(...)— Python tells you something.input(...)— Python asks you something. Returns whatever the user typed.+— sticks two strings together (concatenation).- Variables —
name = "Sam".
Part A: Three kinds of values
Open Thonny and start a new file. Save it as types.py.
A type is a category of value. Python has lots of types, but three matter for almost everything you’ll do:
| Type | What it is | Examples |
|---|---|---|
int | a whole number (integer) | 5, 42, -3, 0 |
float | a decimal number | 3.14, 5.0, -0.5 |
str | text (string) | "Hello", "Sam", "42" |
Notice that 5 and 5.0 look almost the same but are different
types — one is an int, one is a float. And "42" (with quotes)
is a string even though it looks like a number.
See the type with type()
Python has a built-in function called type() that tells you
what type a value is. Try this:
print(type(5))
print(type(5.0))
print(type("Hello"))
print(type("5"))
Save. Run.
The shell shows:
<class 'int'>
<class 'float'>
<class 'str'>
<class 'str'>
Notice the last two: 5.0 is a float, but "5.0" (with quotes)
is a string. The quotes change everything.
Why types matter
Try this:
print(5 + 3)
print("5" + "3")
Run. The shell shows:
8
53
Two completely different answers. Same-looking values; different types; different behavior.
5 + 3is number addition. Five plus three is eight."5" + "3"is string concatenation. “5” stuck together with “3” is “53”.
Python + does different things depending on what it’s
between. With numbers, math. With strings, sticking together.
This matters because input() always returns a string —
even if the user typed a number. Try:
age = input("How old are you? ")
print(type(age))
print(age + 1)
Save. Run. Type your age and press Enter.
You get an error:
TypeError: can only concatenate str (not "int") to str
Translation: “I can’t add a number to a string. They’re different types.”
The fix: convert the string to a number before doing math with it.
Type conversion
Python has functions for converting between types:
| Function | Converts to | Example |
|---|---|---|
int(x) | integer | int("5") is 5 |
float(x) | decimal | float("3.14") is 3.14 |
str(x) | string | str(5) is "5" |
Fix the previous program:
age = int(input("How old are you? "))
print(type(age))
print(age + 1)
The int(input(...)) does two things: gets the user’s input
(as a string), then converts it to an integer.
Save. Run. Type your age. Now you see:
<class 'int'>
[your age plus 1]
The error is gone because age is now an int, and you can add
1 to an int.
This is one of the most common patterns in Python: convert input to the type you actually want to work with.
One more conversion
You might also need str() to go the other direction. Try this
without conversion:
score = 100
print("Your score is " + score + " points!")
Run. Error: TypeError: can only concatenate str (not "int").
Fix it with str():
score = 100
print("Your score is " + str(score) + " points!")
Now the int 100 becomes the string "100", which can be
concatenated with the rest of the message.
(There’s a much nicer way to do this in Python called f-strings — coming next week.)
Checkpoint: You’ve used type() to check the type of at
least three different values, you’ve seen the difference between
5 + 3 and "5" + "3", and you’ve used int() to convert input
to a number for math. This is the natural stop point if class
is cut short.
Part B: A real program with math
Now let’s build something useful with input + types.
Base goal — age in years
Build this:
print("Welcome to the Age Calculator!")
age = int(input("How old are you? "))
years = int(input("How many years from now? "))
future_age = age + years
print("In " + str(years) + " years, you'll be " + str(future_age) + ".")
Run. Type your age (e.g., 12). Type a number of years (e.g., 8). The shell tells you what age you’ll be then.
Walk through what’s new:
- Two
int(input(...))calls to get two numbers. - Math on variables:
age + yearsadds them. str()on the way out so we can concatenate the numbers into the output string.
Tedious but it works. You’ll see why f-strings are nicer next week.
Stretch — decimals
What if you want fractional answers? Use float:
print("Average calculator!")
a = float(input("First number: "))
b = float(input("Second number: "))
c = float(input("Third number: "))
average = (a + b + c) / 3
print("The average is " + str(average))
Now the user can type decimals (like 4.5 or 7.25) and the
result handles fractional averages.
The parentheses in (a + b + c) / 3 matter — they make sure
the addition happens first, then the division. Math order of
operations applies in Python just like math class.
Extension — multi-step calculator
Build a tip calculator. Or a “how many minutes until…” program. Or “convert miles to feet.” Or anything that asks the user for numbers, does some math, and reports the result.
For example, a savings calculator:
print("Savings Calculator")
weekly_save = float(input("How much do you save each week? $"))
weeks = int(input("How many weeks? "))
total = weekly_save * weeks
print("In " + str(weeks) + " weeks, you'll save $" + str(total))
Or a unit converter:
miles = float(input("How many miles? "))
feet = miles * 5280
print(str(miles) + " miles is " + str(feet) + " feet.")
Pick something you’d want to know an answer to, and write the program to compute it.
Wrap-up
Before we leave, share with the room:
- What did your calculator compute?
- Did anyone hit a
TypeErrorthey had to fix? - For the kids who built the savings or unit conversion — does the answer look right?
You learned today how Python tells different kinds of values apart, and how to convert between them. Types are one of the fundamental ideas in programming. Every value has a type. Operations behave differently depending on type. Errors happen when types don’t match.
In Phase 2 you got away with not thinking about types because turtle commands took numbers and you typed numbers. Now that input() is in the mix, type conversion becomes part of every interactive program.
If you missed this session
Open Thonny and start a new file. Save as types.py. Then:
-
Try
print(type(5)),print(type(5.0)),print(type("hi")). See the three types: int, float, str. -
Try
print(5 + 3)andprint("5" + "3"). Notice the different answers. -
Build the age calculator from Part B base:
age = int(input("How old are you? ")) years = int(input("How many years from now? ")) future_age = age + years print("In " + str(years) + " years, you'll be " + str(future_age) + ".") -
Customize: build a calculator that asks for numbers and reports something useful (savings, conversion, average).
About 30 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- What does
int("5.7")do? What doesint(5.7)do? Try both and see. - What does
float("hello")do? Run it and read the error message. - What does
str(5) + str(3)return? Why? - What does
int("5") + int("3")return? Why? - Try
5 + 5.0— what type is the result? (Hint: it’ll be a float. When you mix int and float, Python promotes to float.)
What’s next
Next week we dig deeper into strings — text is the most
common kind of data in real programs (names, addresses,
sentences, file contents). We’ll learn how to slice strings,
search them, change their case, and — most importantly — use
f-strings to format text without all the + str(...) +
mess from today.
Session 3: Strings — text in Python
Session 3: Strings — text in Python
Phase 3 — Python basics · Session 3 of 16
What we’re learning today
Last week’s age calculator had a bunch of awkward
+ str(...) + mess every time we wanted to put a number into a
sentence. Today we fix that with a tool called f-strings, and
we learn how to do useful things with text — change its case,
find pieces of it, count letters, even reverse it. By the end of
class, you’ll have built a “name analyzer” that takes a name and
reports five different facts about it.
You’ll need to remember from last time
- Three types — int, float, str.
- Type conversion —
int(...),float(...),str(...). input(...)always returns a string.- String concatenation —
"Hello, " + name + "!"(the awkward way we did it last week).
Part A: f-strings (the good way)
Open Thonny and start a new file. Save it as strings.py.
Remember last week’s age calculator? It looked like this:
print("In " + str(years) + " years, you'll be " + str(future_age) + ".")
Notice the + and str() everywhere. Annoying.
Here’s the better way:
print(f"In {years} years, you'll be {future_age}.")
Same output. Way cleaner. That’s an f-string.
What’s an f-string
An f-string is a string with an f in front of the opening
quote. Inside, you can put variables in curly braces and
they get inserted automatically.
name = "Sam"
age = 12
print(f"My name is {name} and I'm {age} years old.")
Output: My name is Sam and I'm 12 years old.
The {name} and {age} get replaced with whatever’s in those
variables. No str() needed — Python figures out how to
turn each variable into text for you.
You can also put whole expressions in the curly braces:
age = 12
print(f"Next year you'll be {age + 1}.")
print(f"In ten years you'll be {age + 10}.")
Output:
Next year you'll be 13.
In ten years you'll be 22.
The math runs first; the result goes into the string.
Try it
Build a file with your own f-string demos:
name = "Sam"
age = 12
favorite_color = "blue"
print(f"Hi, my name is {name}.")
print(f"I am {age} years old.")
print(f"My favorite color is {favorite_color}.")
print(f"In 7 years I'll be {age + 7}.")
Save. Run.
That’s the entire f-string thing. Three rules:
- Put
fin front of the opening quote. - Put variables in
{curly braces}. - Done.
“From now on, always use f-strings when building strings with variables in them. The old
+ str(...) +way still works, but f-strings are easier to read and easier to write.”
Useful string methods
A method is a function that belongs to a value. You call it
with a . after the value:
| Method | What it does |
|---|---|
s.upper() | returns the string in UPPERCASE |
s.lower() | returns the string in lowercase |
s.strip() | removes whitespace from the start and end |
s.replace("old", "new") | replaces parts of the string |
s.split(" ") | splits the string into a list at each space |
Try these:
name = "Sam"
print(name.upper())
print(name.lower())
print(name.replace("a", "X"))
Output:
SAM
sam
SXm
These methods don’t change the variable — they return a new
string. So name is still "Sam" after name.upper().
If you want to change name itself, you’d assign back to it:
name = name.upper()
.strip() — clean up input() (do this every time)
There’s a quiet bug that gets people for years: input() and
file reading often hand you strings with invisible whitespace
on the ends. A trailing space. A leftover newline character.
That whitespace makes string comparisons silently fail:
guess = input("Guess: ")
if guess == "yes":
print("You got it!")
If the kid types yes and hits enter, this works fine — but if
their input has a trailing space (easy to hit by accident), the
comparison breaks. The string "yes " is not equal to
"yes".
Fix: call .strip() on every input() you ever do.
guess = input("Guess: ").strip()
Now "yes ", "yes", and " yes " all become "yes". The
comparison works.
From now on, every
input()in the curriculum gets.strip()chained on it. It’s one of those “always do this” habits — like wearing a seatbelt. Free protection.
.split() — break a string into a list of pieces
If a string has multiple words separated by spaces, .split(" ")
breaks it into a list of those words. (Lists are coming in
Session 8 — for now, just know it gives you back several pieces
you can use.)
sentence = "the quick brown fox"
words = sentence.split(" ")
print(words)
# Output: ['the', 'quick', 'brown', 'fox']
You can split on any character, not just spaces. s.split(",")
splits on commas. Useful when input comes in as a list of things
the user typed: "red,blue,green".split(",") gives you each
color separately.
We’ll use .split() heavily once we hit lists. For now, just
know it exists and what it does.
How long is a string?
len(s) tells you how many characters are in a string:
name = "Caleb"
print(len(name)) # 5
len is a function (not a method), so the syntax is len(name),
not name.len().
Checkpoint: You’ve built a Python file that uses an f-string
with at least two variables, and you’ve called .upper() (or
.lower()) on a string and printed the result. This is the
natural stop point if class is cut short.
Part B: Indexing, slicing, and a name analyzer
Strings are made of characters in order. Python lets you grab specific characters or pieces of a string by position.
Indexing
A string is like a row of boxes, numbered starting from 0:
"Caleb"
C a l e b
0 1 2 3 4
To get one character, use square brackets:
name = "Caleb"
print(name[0]) # C
print(name[1]) # a
print(name[4]) # b
You can also count from the end using negative numbers:
print(name[-1]) # b (last character)
print(name[-2]) # e (second-to-last)
Slicing
To get a piece of a string, use [start:end]. The slice
includes start but stops before end:
name = "Caleb"
print(name[0:3]) # Cal (characters 0, 1, 2 — not 3)
print(name[1:4]) # ale (characters 1, 2, 3)
print(name[2:]) # leb (from 2 to the end)
print(name[:3]) # Cal (from the start to 3)
You can also slice with a step. The most useful step trick:
reverse a string with [::-1]:
print(name[::-1]) # belaC
The double colon means “take every step character” — and -1
means “step backward.” Don’t memorize the syntax; just remember
[::-1] reverses a string.
Build a name analyzer
Combine everything from Part A and Part B:
name = input("What's your name? ").strip()
print(f"Your name is {name}.")
print(f"It has {len(name)} characters.")
print(f"It starts with '{name[0]}' and ends with '{name[-1]}'.")
print(f"In all caps: {name.upper()}")
print(f"Reversed: {name[::-1]}")
Save. Run. Type your name.
(Notice .strip() on the input() — that’s the new habit.)
The shell shows five facts about your name, all using f-strings.
That’s the base goal for today.
Stretch — vowel counter
Count how many vowels are in the name. Use in to check if a
character is in a string:
name = input("What's your name? ").lower()
vowel_count = 0
for letter in name:
if letter in "aeiou":
vowel_count = vowel_count + 1
print(f"{name} has {vowel_count} vowels.")
This combines:
.lower()to make the comparison case-insensitivefor letter in name:— a loop that goes through each letter of the stringif letter in "aeiou":— checks if the letter is one of the vowels- A counter variable that increases when we find a vowel
- An f-string for the result
The for letter in name: syntax is new but easy: it’s a for
loop that visits each character of the string in order. We’ll
see this pattern a lot more once we get to lists.
Extension — palindrome checker
A palindrome is a word that reads the same forwards and backwards. “Mom” is a palindrome. “Racecar” is a palindrome. “Caleb” is not.
Build a palindrome checker:
word = input("Enter a word: ").lower()
if word == word[::-1]:
print(f"'{word}' is a palindrome!")
else:
print(f"'{word}' is not a palindrome.")
Try it with mom, racecar, taco, level, wasitacaroracatisaw.
The .lower() makes the comparison case-insensitive (so “Mom”
and “mom” both work). The word[::-1] reverses the string. If
the original equals the reverse, it’s a palindrome.
Wrap-up
Before we leave, share with the room:
- What did you put into the name analyzer? Anything funny?
- For the kids who did the vowel counter — does your name have more vowels than you expected?
- For the kids who did the palindrome checker — what’s the longest palindrome you found?
You learned today the most-used string operations in real Python: f-strings (for output), indexing (for getting one character), slicing (for getting pieces), and a few methods (for transforming).
F-strings will be in almost every program you write from now
on. They’re that common. The old + str(...) + mess is
officially retired.
If you missed this session
Open Thonny and start a new file. Save as strings.py. Then:
-
Build an f-string demo:
name = "Sam" age = 12 print(f"My name is {name} and I'm {age} years old.") print(f"Next year I'll be {age + 1}.") -
Try
.upper(),.lower(),len()on a string. Print the results. -
Try indexing:
name[0],name[-1],name[1:3]. -
Build the name analyzer from Part B base goal.
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- F-string formatting:
f"{age:>5}"right-justifies in 5 spaces.f"{price:.2f}"shows 2 decimal places. There are many — try them and see. s.startswith("hello")ands.endswith("!")return True/False. Useful in conditionals.s.find("substring")returns the position where a substring first appears (or-1if it’s not in the string).- Multi-line strings:
"""Hello\nWorld"""(triple quotes for strings that span lines).
What’s next
Next week we deepen conditionals — combining multiple
conditions with and, or, and not. Combined with f-strings,
you’ll be able to write programs that respond to multiple things
at once and tell the user clearly what they decided.
Session 4: Conditionals — combining decisions
Session 4: Conditionals — combining decisions
Phase 3 — Python basics · Session 4 of 16
What we’re learning today
You already know if, elif, and else from Phase 2. Today
we add three small but powerful words — and, or,
and not — that let you combine multiple conditions into
one decision. By the end of class, you’ll have built a
“rollercoaster eligibility checker” that asks the user several
things and tells them if they can ride.
You’ll need to remember from last time
- f-strings —
f"Hello, {name}!". - Comparison operators —
>,<,==,!=,>=,<=. if/elif/elsefrom Phase 2 Session 6 — “if condition, do this; elif condition, do that; else, do the other.”int(input(...))for numeric input.
Part A: and, or, not
Open Thonny and start a new file. Save it as decisions.py.
In real life, decisions usually involve multiple things at once. “Can I ride the rollercoaster?” depends on age AND height. “Can I get my driver’s license?” depends on age OR a special permit. “Should I take an umbrella?” depends on whether it’s NOT sunny.
Python has three words for combining conditions:
| Word | What it means | Example |
|---|---|---|
and | both must be true | age >= 13 and height >= 50 |
or | at least one must be true | name == "Sam" or name == "Alex" |
not | the opposite | not age >= 18 |
and — both conditions
age = 15
height = 60
if age >= 13 and height >= 50:
print("You can ride!")
else:
print("Sorry, not yet.")
The if runs only if both parts are true. Age must be at
least 13, and height must be at least 50. If either fails, the
combined condition is false.
Try changing age = 12. The condition fails (because 12 isn’t
= 13). Try changing
height = 40. Same — fails because height isn’t >= 50.
or — at least one
name = input("What's your name? ")
if name == "Sam" or name == "Alex" or name == "Jordan":
print("Welcome, friend!")
else:
print(f"Hi, {name}.")
The if runs if the name matches any of the three. The or
operator means “at least one of these has to be true.”
not — the opposite
is_raining = True
if not is_raining:
print("Leave the umbrella at home.")
else:
print("Bring your umbrella!")
not flips a true to false (or vice versa). not True is
False. not False is True.
not is most useful when the variable already has a yes/no
meaning. not is_raining reads naturally: “if it’s not
raining.”
Combining all three
You can mix them. Here’s a full example:
age = 14
has_permission = True
if (age >= 13 or has_permission) and not age < 8:
print("You can play this game.")
else:
print("Not allowed.")
This checks: “if (you’re 13 or older OR you have permission), AND (you’re NOT under 8), then you can play.”
The parentheses group the conditions like math. and is
checked after or by default — using parentheses makes the
order explicit and makes the code easier to read.
Tip: when conditions get complicated, put each part on its own line. Python lets you split a long condition with backslashes or by surrounding it with parentheses. Most programmers use parentheses. We’ll see that in the rollercoaster project.
Checkpoint: You’ve used and, or, and not in at least
one program, and you’ve seen how Python evaluates a combined
condition. This is the natural stop point if class is cut
short.
Part B: The rollercoaster eligibility checker
Time to build something that uses all this.
What you’re building
A program that asks the user their age and height, then tells them whether they can ride the rollercoaster. The rules:
- You must be at least 13 years old.
- You must be at least 48 inches tall.
Both conditions must be true to ride.
Base goal
print("Welcome to the Rollercoaster Eligibility Checker!")
age = int(input("How old are you? "))
height = int(input("How tall are you, in inches? "))
if age >= 13 and height >= 48:
print("You can ride! Have fun!")
else:
print("Sorry, you can't ride this one.")
Save. Run. Try a few combinations:
age = 15, height = 60— should pass.age = 10, height = 70— should fail (too young).age = 14, height = 40— should fail (too short).
That’s the base goal. Working program, simple combined condition.
Stretch — tell them WHY
A vague “sorry, you can’t ride” isn’t very helpful. Use multiple branches to tell the user exactly what’s wrong:
if age >= 13 and height >= 48:
print("You can ride! Have fun!")
elif age >= 13 and height < 48:
print("You're old enough, but you're a little too short.")
elif age < 13 and height >= 48:
print("You're tall enough, but you're a little too young.")
else:
print("You're both too young and too short. Come back in a few years!")
Now the program gives specific feedback for each failure case. Four possible outcomes; one branch per outcome.
Extension — refactor with intermediate variables
The repeated age >= 13 and height >= 48 checks are awkward.
Save the results in named variables for cleaner code:
print("Welcome to the Rollercoaster Eligibility Checker!")
age = int(input("How old are you? "))
height = int(input("How tall are you, in inches? "))
old_enough = age >= 13
tall_enough = height >= 48
if old_enough and tall_enough:
print("You can ride! Have fun!")
elif old_enough:
print("You're old enough, but you're a little too short.")
elif tall_enough:
print("You're tall enough, but you're a little too young.")
else:
print("You're both too young and too short. Come back in a few years!")
Notice what’s different:
old_enough = age >= 13saves the result of the comparison (a True or False value) in a variable. This is called a boolean — a value that’s eitherTrueorFalse.- The
ifstatements now read more like English. - The repetition is gone.
This is a common trick in real code: when conditions are repeated or complex, give them names.
You could push this even further:
if old_enough and tall_enough:
print("You can ride! Have fun!")
else:
if not old_enough:
print("- You're too young.")
if not tall_enough:
print("- You're too short.")
print("Try again later!")
Now you list all the failures, not just the first one. That
uses not to invert each check.
Wrap-up
Before we leave, share with the room:
- For the kids who built the base — did your eligibility check work for the test cases?
- For the stretch — was anyone surprised by the “old enough but too short” message?
- For the extension — did the named variables version feel cleaner?
You learned today the three small words that make Python’s
conditionals genuinely powerful. and, or, not turn
simple yes/no checks into real decisions about complicated
situations. Every program that handles “what should we do here?”
ends up using these.
You also met booleans — values that are True or False.
Booleans are the third major type after numbers and strings,
and you’ll see them constantly from here on.
If you missed this session
Open Thonny and start a new file. Save as decisions.py. Then:
-
Try basic
and,or,notexamples:age = 15 if age >= 13 and age < 20: print("Teenager.") -
Build the rollercoaster eligibility checker (Part B base):
- Ask for age and height
- Check
age >= 13 and height >= 48 - Print “you can ride” or “sorry”
-
Try the stretch — separate branches for each failure case.
About 30 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- The order of
orandandmatters when they’re mixed.True or True and FalseisTruebecauseandis evaluated first. Use parentheses when in doubt:(True or True) and FalseisFalse. TrueandFalse(with capital T and F) are Python’s built-in booleans. You can assign them directly:is_raining = True.- The
bool()function converts other values to True/False.bool(1)is True;bool(0)is False;bool("")is False;bool("hello")is True. Try them. - Compound conditions with three or more parts:
if x > 0 and y > 0 and z > 0:— works for any number of parts.
What’s next
Next week we deepen loops and meet the Thonny debugger — a tool that lets you watch your program run line by line. The debugger is the single most useful thing for figuring out why your program isn’t doing what you thought it would. By the end of next week, you’ll have a new debugging superpower.
Session 5: Loops (deeper) and the Thonny debugger
Session 5: Loops (deeper) and the Thonny debugger
Phase 3 — Python basics · Session 5 of 16
What we’re learning today
Two new things today, both useful: the while loop (a different
kind of loop from the for loop you already know), and the
Thonny debugger — a tool that lets you watch your program
run line by line and see exactly what’s happening inside. The
debugger is the single most useful tool for figuring out why
your code isn’t doing what you thought it would. By the end of
class, you’ll have a new debugging superpower.
You’ll need to remember from last time
forloops —for i in range(N):runs N times.- Combined conditionals —
and,or,not. - Booleans —
TrueandFalse. - Indentation — code inside a loop or
ifis indented.
Part A: The while loop
Open Thonny and start a new file. Save it as while_loop.py.
What while does
A for loop runs a known number of times — for i in range(10):
runs ten times. Period.
A while loop runs as long as a condition is true. The
program checks the condition before each iteration; if it’s
true, run the body and check again; if it’s false, stop.
count = 1
while count <= 5:
print(f"Count is {count}")
count = count + 1
print("Done!")
Save. Run.
Count is 1
Count is 2
Count is 3
Count is 4
Count is 5
Done!
Walk through what happened:
count = 1— start the variable at 1.while count <= 5:— check: is 1 <= 5? Yes. Run the body.- Inside: print, then
count = count + 1(now count is 2). - Back to the
whilecheck: is 2 <= 5? Yes. Run again. - …continue until count is 6. Now
6 <= 5is False. Stop. - After the loop, “Done!” prints.
The structure: while CONDITION: then indented body. Same
indentation rule as if and for.
Why while instead of for?
for loops are great when you know how many times to repeat —
“draw 10 squares” or “for each letter in this word.”
while loops are great when you don’t know how many times —
“keep asking the user until they give a valid answer” or “keep
playing until the player wins.”
Try this — a guess-the-number style program:
secret = 7
guess = 0 # start with anything that's not the secret
while guess != secret:
guess = int(input("Guess a number: "))
if guess != secret:
print("Nope, try again.")
print(f"You got it! The number was {secret}.")
Save. Run. Try to guess 7. The loop runs until you guess right.
This wouldn’t be easy with a for loop — you don’t know how
many guesses it’ll take.
Watch out for infinite loops
The dangerous thing about while loops: if the condition never
becomes false, the loop runs forever. This is called an
infinite loop.
count = 1
while count <= 5:
print(count)
# OOPS: forgot to update count!
Run this. The shell prints 1 over and over forever. Press
the red Stop button in Thonny (or Ctrl-C in the shell) to
kill it.
Every while loop must do something inside that eventually
makes the condition false. Most often that’s updating a
variable (like count = count + 1 in the example).
break
Sometimes you want to exit a loop early. The break statement
does that:
while True:
answer = input("Type 'quit' to stop: ")
if answer == "quit":
break
print(f"You said: {answer}")
print("Goodbye!")
while True: is a deliberate “infinite” loop — but break
exits it when the user types “quit.” Common pattern.
Checkpoint: You’ve written a while loop that runs at
least three times before stopping (either by a count
variable or by user input). This is the natural stop point
if class is cut short.
Part B: The Thonny debugger
Now for the debugging superpower.
What the debugger is
Most of the time when your code is wrong, you read the code, spot the bug, fix it. But sometimes — especially with loops and multi-step programs — you can’t tell what’s wrong by reading. Variables change in ways you didn’t expect. The control flow goes somewhere you didn’t intend.
The debugger lets you watch your program run line by line and see the value of every variable at every step. It’s like having superpowered slow-motion vision.
A buggy program
Type this exactly. There’s a deliberate bug:
word = "hello"
vowel_count = 0
for letter in word:
if letter in "aeiou":
vowel_count = 1
print(f"Vowels in '{word}': {vowel_count}")
Save as vowel_bug.py. Run.
Vowels in 'hello': 1
But “hello” has two vowels (e, o). Why does the program say 1? Reading the code carefully, you might spot it. But let’s use the debugger to see the bug happen.
Running with the debugger
In Thonny, look at the toolbar. Next to the green Run button, there are some other buttons. The one that looks like a bug icon is “Debug current script.”
Click the bug icon (or press Ctrl-F5).
Two things change:
- The program pauses at the first line, with that line highlighted.
- A panel appears (often on the right) showing variables and their values. Right now, no variables exist yet.
Stepping through
Find the “Step over” button (looks like an arrow stepping over a line — usually called Step Over, F6 on most systems). Click it once.
The first line ran. Look at the variables panel — word is now
"hello". You can see the variable that just got created.
Click “Step Over” again. Now vowel_count is 0.
Click again. The for loop starts. Watch what letter is — it’s
"h". The loop is on its first iteration, looking at the first
character of “hello”.
Click again. The if checks: is "h" in "aeiou"? No. Skip
the body.
Click again. Loop continues. Now letter is "e".
Click again. if checks: is "e" in "aeiou"? Yes! Run the
body.
Click again. vowel_count = 1. The variables panel updates —
vowel_count is now 1.
Click again. Loop continues. Now letter is "l". if is
False, skip.
Click. letter is "l" again. Skip.
Click. letter is "o". if is True! Run the body.
Click. vowel_count = 1. Wait — it’s still 1, not 2.
That’s the bug. Every time the program finds a vowel, it sets
vowel_count to 1 (overwriting the previous value), instead
of adding 1 to the existing count.
Fix the bug
Change vowel_count = 1 to vowel_count = vowel_count + 1.
Save. Run normally (green button). Output:
Vowels in 'hello': 2
You found and fixed a bug by watching the program run. That’s the debugger.
Why this matters
You’ll often be in this situation: your program runs but does the wrong thing, and you don’t know why. Reading the code more carefully helps sometimes. The debugger helps every time — it shows you exactly what’s happening.
The same workflow works for any Python program:
- Click the bug icon to start debugging.
- Watch the variables panel.
- Click “Step Over” to advance one line at a time.
- Notice when a variable becomes wrong. That’s where the bug is.
You can also click “Resume” (or F8) to run normally until the next error or until the program ends.
Try it
Open one of your earlier programs (the rollercoaster checker, the name analyzer, or the age calculator). Run it with the debugger. Step through. Watch the variables change.
You don’t need to find a bug — just practice using the debugger. The fluency you build by using it on working code makes it much easier to use when you actually need it.
Wrap-up
Before we leave, share with the room:
- What’s a situation where a
whileloop made more sense than aforloop? - For the debugger demo — was it surprising to see the vowel_count being set to 1 every time?
- For the kids who debugged an earlier program — did anything surprise you about how it ran?
You learned today the most important debugging skill in any programming language: watching your program execute, step by step. The Thonny debugger is small and friendly; debuggers in bigger languages (like JavaScript or Java) work the same way but with more features. The mental model carries over.
You also learned while — the loop for “until something is
true.” Combined with for (for known counts), these are the
two loop tools you’ll use for the rest of your programming
life.
If you missed this session
Open Thonny and start a new file. Save as while_loop.py. Then:
-
Build a
whileloop that counts up:count = 1 while count <= 5: print(f"Count is {count}") count = count + 1Save, run.
-
Build the guess-the-number version (Part A above).
-
Open
vowel_bug.py(Part B). Type it in with the bug. Run it normally — see the wrong answer (1 vowel for “hello”). Now click the bug icon in Thonny instead of the green Run button. Step through with F6. Watch the variables panel. Find wherevowel_count = 1is wrong (should bevowel_count + 1). Fix it.
About 40 minutes. The debugger takes a few minutes to get comfortable with — keep stepping through.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Step Into (F7) — for stepping inside a function call, not just over it. Useful when you have a function with a bug.
- Set a breakpoint — click in the leftmost margin next to a line number. The program will pause when it reaches that line. Useful for skipping past parts you’ve already verified.
continue(in loops) — likebreak, but skips the rest of the current iteration and moves to the next one. Less useful thanbreak; mention if asked.whilewith multiple exit conditions —while not done and not error:. Combine booleans withwhile.
What’s next
Next week we deepen functions — specifically, functions
that return a value (like len("hello") returns 5). Until
now, functions just did things. Next week they’ll start
giving you results back, which is what makes them genuinely
powerful.
Session 6: Functions that return values
Session 6: Functions that return values
Phase 3 — Python basics · Session 6 of 16
What we’re learning today
You’ve been writing functions since Phase 2. Until now, those
functions just did things — drew shapes, printed messages.
Today you’ll learn how to write functions that give you back
a value — like len("hello") returns 5, or int("42")
returns 42. Functions that return values are how real Python
programs are organized. By the end of class, you’ll have
written a grade calculator that uses two of your own
return-value functions.
You’ll need to remember from last time
whileloops —while condition:runs as long as the condition is true.- The Thonny debugger — bug icon button, Step Over (F6).
- Functions —
def name(parameters):followed by an indented body. int(input(...))— the canonical pattern for numeric user input.
Part A: return
Open Thonny and start a new file. Save it as returns.py.
Two kinds of functions
You’ve seen functions that do something:
def greet(name):
print(f"Hello, {name}!")
This function prints a message. After it runs, it’s done. Nothing comes back to the calling code.
But you’ve also used functions that give you something back:
length = len("hello") # length is now 5
upper_name = "sam".upper() # upper_name is now "SAM"
age = int("42") # age is now 42
len, upper, and int all return values. That’s the
new word: a function “returns” a value when it gives something
back to the code that called it.
Today you’ll write your own return-value functions.
Your first return
Type this:
def double(x):
return x * 2
result = double(5)
print(result)
Save. Run. The shell shows 10.
Walk through what’s new:
def double(x):— function takes one parameter.return x * 2— the function calculatesx * 2and returns it to the caller.result = double(5)— call the function with 5; whatever the function returns gets put intoresult.print(result)— print the returned value.
The return statement is what’s new. When return runs, the
function stops immediately and gives the value back to
whoever called it.
Where the value goes
A function call (double(5)) is now an expression with a
value (in this case, 10). That means you can use it
anywhere a value would work:
print(double(5)) # prints 10 directly
print(double(5) + 3) # prints 13 (10 + 3)
big = double(double(5)) # double of double — big is 20
That last one is a little wild: double(5) returns 10, then
double(10) returns 20. Functions calling functions, all in
one line.
Multiple parameters, one return
def add(a, b):
return a + b
print(add(3, 4)) # 7
print(add(10, 20)) # 30
Same pattern: take input parameters, do math, return the answer.
return ends the function
Once return runs, the function stops. Anything after return
is dead code:
def double(x):
return x * 2
print("This will never run!") # never executes
Useful when you want to exit early based on a condition:
def safe_divide(a, b):
if b == 0:
return 0 # exit early to avoid divide-by-zero
return a / b
If b is 0, the function returns 0 and stops. Otherwise, it
runs the second return. Same way break works in loops, but
for functions.
Checkpoint: You’ve written at least one function that returns a value, called it, and used the returned value (saved it in a variable, printed it, or used it in an expression). This is the natural stop point if class is cut short.
Part B: A grade calculator
Time to build something with multiple return-value functions working together.
What you’re building
A program that asks for three test scores, calculates the average, and tells the user their letter grade. Two functions:
average(a, b, c)— returns the average of three numbersletter_grade(score)— returns “A”, “B”, “C”, “D”, or “F” based on the score
Then the main program calls both.
Build the average function
def average(a, b, c):
return (a + b + c) / 3
# test it
print(average(80, 90, 100)) # should print 90.0
print(average(70, 75, 80)) # should print 75.0
Run. Both should match. The function takes three numbers, returns their average.
The parentheses around (a + b + c) matter — they ensure the
addition happens before the division. Same order-of-operations
rule as math class.
Build the letter_grade function
def letter_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
# test it
print(letter_grade(95)) # A
print(letter_grade(82)) # B
print(letter_grade(50)) # F
Notice: each branch has its own return. As soon as one
returns, the function stops — no need for an explicit else
in some cases. (We use elif/else here for clarity, but you
could also write five separate ifs ending with return.
Both work.)
Combine them
def average(a, b, c):
return (a + b + c) / 3
def letter_grade(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
# main program
print("Enter your three scores:")
math = float(input("Math: "))
science = float(input("Science: "))
english = float(input("English: "))
avg = average(math, science, english)
grade = letter_grade(avg)
print(f"Your average is {avg:.1f}")
print(f"Your letter grade is {grade}")
Save. Run. Type some scores. The program reports your average and letter grade.
Notice what happened:
avg = average(math, science, english)— the average function’s return value goes intoavg.grade = letter_grade(avg)— the letter_grade function’s return value goes intograde. It usesavg, which came from the previous function.
One function’s output is the next function’s input. This is one of the most common patterns in real programs.
The :.1f in the f-string is formatting — it means “show
this float with 1 decimal place.” 90.0 is shown as 90.0,
not 90.00000000001 or whatever the underlying value is. Try
:.2f for two decimal places, :.0f for no decimal point.
That’s the base goal.
Stretch — variables stay inside
Try this experiment. Add a variable inside a function:
def double(x):
secret = x * 2
return secret
print(double(5)) # 10
print(secret) # ERROR
Run. The first print is 10. The second gives:
NameError: name 'secret' is not defined
Why? secret is a variable created inside the function.
After the function ends, the variable goes away. It’s
local to the function — only the code inside can see it.
This is called scope. Variables inside a function live only inside the function. To get a value out, return it.
This is why return matters so much: it’s the only good way to
get a value out of a function.
Extension — refactor the rollercoaster
Open last week’s rollercoaster eligibility checker (or rebuild it). Refactor it to use return-value functions:
def is_old_enough(age):
return age >= 13
def is_tall_enough(height):
return height >= 48
def can_ride(age, height):
return is_old_enough(age) and is_tall_enough(height)
# main program
age = int(input("How old are you? "))
height = int(input("How tall are you? "))
if can_ride(age, height):
print("You can ride!")
else:
print("Sorry, not yet.")
Now the program is built out of small, focused, named
functions. Each one returns a True/False (a boolean — remember
those?). The main if reads almost like English: if can_ride(age, height):.
This is how real Python code is structured: small functions that return values, composed together.
Wrap-up
Before we leave, share with the room:
- For the kids who built the grade calculator — what scores produce a B?
- For the stretch — were you surprised that
secretwasn’t available outside the function? - For the extension — does the rollercoaster code read more like English with named functions?
You learned today the most important concept in writing reusable code: functions that return values. Most real Python programs are organized as collections of small functions that take inputs and return outputs. You can think about each function as a little machine: pour something in the top, get something out the bottom.
You also learned about scope — variables inside a function
are local. They don’t leak out. The only way to get a value
out is to return it. This is a feature, not a limitation —
it’s what lets functions stay independent and reusable.
If you missed this session
Open Thonny and start a new file. Save as returns.py. Then:
-
Build a
doublefunction:def double(x): return x * 2 print(double(5)) # 10 -
Build an
addfunction with two parameters that returns their sum. -
Build the grade calculator from Part B (average and letter_grade functions).
-
Try the stretch — see what happens when you try to print a variable that was defined inside a function.
About 35-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- A function can return anything — a number, a string, a
boolean, even another value built from those. Try writing a
function
describe(age)that returns a string like"adult"or"kid". - A function can return multiple values using a tuple:
return name, age, height. The caller unpacks withn, a, h = my_function(). Try it. (We won’t use this much in Phase 3, but it’s a Python feature.) - Recursive functions — functions that call themselves. Mind-bending; useful for some problems. Don’t worry about it yet but know it exists.
- A function with no
returnreturnsNoneautomatically. Trydef nothing(): passthenprint(nothing()). You’ll seeNone.
What’s next
Next week is your first real Python project — a number-guessing game. The computer thinks of a number; you have to guess it; the computer tells you “too high” or “too low” until you get it right. It uses everything you’ve learned: input, output, conditionals, loops, functions, return values. Game on.
Session 7: Number-guessing game
Session 7: Number-guessing game
Phase 3 — Python basics · Session 7 of 16
What we’re learning today
Today we put together everything from Sessions 1-6 into your first real Python game. The computer thinks of a secret number; you have to guess it; the computer tells you “too high” or “too low” until you get it right. By the end of class, you’ll have a game you can play (and have your friends play) — built entirely from the building blocks you’ve already learned.
You’ll need to remember from last time
printandinputfor output and asking the user.int(input(...))to get a number from the user.if/elif/elsefor branching.whileloops for “until something is true.”breakto exit a loop early.- f-strings for output with variables.
That’s pretty much everything. Today is integration.
Part A: Build the basic game
Open Thonny and start a new file. Save it as guessing_game.py.
The plan
Before writing any code, talk through what the game does:
- The computer picks a secret number between 1 and 100.
- The user guesses a number.
- If the guess is too low, say so. If too high, say so. If correct, celebrate and stop.
- Repeat from step 2 until the user gets it right.
That’s three new things to learn (random numbers, comparing guesses, looping until correct) plus stuff you already know (input, print, conditionals).
Step 1: Random numbers
To pick a random number, we use Python’s random module:
import random
secret = random.randint(1, 100)
print(secret) # for testing — we'll remove this later
import random brings in the random module (just like import turtle did in Phase 2). random.randint(1, 100) returns a
random whole number from 1 to 100, including both ends.
Save and run a few times. Each run gives a different number.
The print(secret) is just so we can see it during development.
We’ll remove it for the actual game.
Step 2: Get a guess
import random
secret = random.randint(1, 100)
print(secret) # for testing
guess = int(input("Guess a number from 1 to 100: "))
print(f"You guessed {guess}.")
Run. Type a number when prompted. The program echoes it back.
Familiar from Session 2’s int(input(...)) pattern.
Step 3: Compare and respond
import random
secret = random.randint(1, 100)
print(secret)
guess = int(input("Guess a number from 1 to 100: "))
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print("You got it!")
Run. Now the program tells you if your guess was too high, too low, or correct.
But there’s a problem: the program runs the input only once. You guess wrong, the program ends. Time to add a loop.
Step 4: Loop until correct
We want to keep asking until the user guesses right. That’s a
classic while loop pattern from Session 5.
import random
secret = random.randint(1, 100)
print(secret) # for testing
while True:
guess = int(input("Guess a number from 1 to 100: "))
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print("You got it!")
break
The while True: loop runs forever until break. Inside, we
ask, compare, and either keep going or break out.
Save and run. Try to guess. Keep going until you get it right. The program ends when you do.
Step 5: Remove the cheating print
Now that the game works, remove (or comment out) the
print(secret) line at the top. The user shouldn’t see the
secret!
import random
secret = random.randint(1, 100)
while True:
guess = int(input("Guess a number from 1 to 100: "))
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print("You got it!")
break
That’s the base game. A complete, playable number-guessing game in 11 lines of Python.
Checkpoint: You have a working number-guessing game that generates a random number, asks for guesses, and tells the user when they’re right (or whether they’re too high/too low). This is the natural stop point if class is cut short.
Part B: Make it a real game
The base game works but is bare. Let’s add features.
Stretch — count attempts
Players want to know how well they did. Count attempts:
import random
secret = random.randint(1, 100)
attempts = 0
print("I'm thinking of a number between 1 and 100.")
while True:
guess = int(input("Your guess: "))
attempts = attempts + 1
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print(f"You got it in {attempts} tries!")
break
The variable attempts starts at 0, increases with each guess,
and the final message uses it.
Try it. How few attempts can you do? (With perfect binary search — guess 50, then 25 or 75, etc. — you can always win in 7 or fewer tries for a number from 1 to 100.)
Extension — limited tries
Add a lose condition: the user gets only N attempts. If they run out, the computer wins.
import random
secret = random.randint(1, 100)
attempts = 0
max_tries = 10
won = False
print(f"I'm thinking of a number between 1 and 100. You have {max_tries} tries.")
while attempts < max_tries:
attempts = attempts + 1
guess = int(input(f"Try {attempts}/{max_tries}: "))
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print(f"You got it in {attempts} tries!")
won = True
break
if not won:
print(f"You ran out of tries! The number was {secret}.")
What’s new:
max_tries = 10— the limit, in a variable so it’s easy to change.won = False— a boolean to track whether the user won.while attempts < max_tries:— loop condition includes the attempt counter.- After the loop, an
if not won:to handle the lose case.
Now the game is genuinely competitive — there’s a chance to lose.
Extension — play again
Wrap the whole game in another loop so the user can play again:
import random
while True:
# ... (the entire game from above) ...
again = input("\nPlay again? (y/n): ")
if again.lower() != "y":
print("Thanks for playing!")
break
Now after the game ends (win or lose), the user is asked if they want another round. If they type “y” (or “Y”), another game starts. Anything else and the program quits.
That’s a play loop — common pattern in real games.
Extension++ — a clean version with functions
If you’ve finished everything else, refactor the game into functions:
import random
def play_game():
secret = random.randint(1, 100)
attempts = 0
max_tries = 10
print(f"I'm thinking of a number between 1 and 100. You have {max_tries} tries.")
while attempts < max_tries:
attempts = attempts + 1
guess = int(input(f"Try {attempts}/{max_tries}: "))
if guess < secret:
print("Too low!")
elif guess > secret:
print("Too high!")
else:
print(f"You got it in {attempts} tries!")
return True
print(f"You ran out of tries! The number was {secret}.")
return False
# main loop
while True:
play_game()
again = input("\nPlay again? (y/n): ")
if again.lower() != "y":
print("Thanks for playing!")
break
The whole game is in one function (play_game). The function
returns True if the user won and False if they lost (we
don’t actually use the return value here, but it’s good
practice). The main code is just two lines: play, ask if they
want to play again.
This is how real games are organized — small functions for each piece, a simple top-level loop calling them.
Wrap-up
Before we leave, share with the room:
- What’s the fewest tries anyone got the number in?
- For the kids who built the limited-tries version — did anyone lose? Did the lose message work right?
- For the kids who refactored into functions — did the code feel cleaner?
You built a complete game today. A complete game. With random elements, user input, win and lose conditions, optional replay. That’s the same shape as a thousand commercial puzzle games — yours is just simpler.
Most importantly, you built it from stuff you already knew. No new programming concepts, just a new combination. This is what programming starts to feel like once you have the fundamentals: you take the parts you know and arrange them into something new.
If you missed this session
Open Thonny and start a new file. Save as guessing_game.py.
Then build the game step by step:
import randomandsecret = random.randint(1, 100). Print secret while testing.- Ask for a guess with
int(input(...)). - Compare with
if/elif/else. Print “too high” / “too low” / “correct.” - Wrap in a
while True:loop with abreakon the correct guess. - Remove the print(secret) once it works.
About 30-40 minutes for the base game. Then try adding attempt counting (Part B stretch). If you have time, try the limited tries or play-again extensions.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Hint system — if the guess is within 5 of the secret, print “Very close!” instead of just “too high/too low.”
- Difficulty levels — at the start, ask “easy / medium / hard?” Each one uses a different range (1-50, 1-100, 1-500) or different max tries (15 / 10 / 5).
- Track best score — across multiple games, remember the fewest tries it took to win.
- Reverse the game — the user picks a number, and the computer guesses (using binary search). Trickier — give it a shot if you’re feeling ambitious.
What’s next
Next week we learn about lists — Python’s way of holding multiple values in a single variable. Lists are arguably the most important data structure in all of Python; they unlock a huge range of programs you couldn’t write before.
Session 8: Lists — collections of values
Session 8: Lists — collections of values
Phase 3 — Python basics · Session 8 of 16
What we’re learning today
Until now, every variable has held one thing — one number, one string, one True/False. Today you’ll learn how to hold many things in a single variable. That’s a list, and it’s arguably the most important data type in all of Python. By the end of class, you’ll have built a “favorites collector” program that lets the user enter as many things as they want and then shows them all back.
You’ll need to remember from last time
- Variables can hold a value (number, string, boolean).
forloops —for i in range(N):.while True:andbreakfor “loop until something happens” patterns.input(...)for getting input from the user.- f-strings for formatting output.
Part A: What lists are and how to use them
Open Thonny and start a new file. Save it as lists.py.
Making a list
A list is a value that holds multiple other values, in order. You make one with square brackets:
colors = ["red", "blue", "green", "yellow"]
print(colors)
Save. Run.
['red', 'blue', 'green', 'yellow']
The variable colors holds four strings, all in one list.
A list can hold any kind of value — strings, numbers, even booleans:
ages = [12, 14, 9, 16]
mixed = ["Alex", 12, True]
empty = []
The last one ([]) is an empty list — a list with nothing in
it yet. Useful when you want to fill it up gradually.
Getting items by index
Lists use the same [index] syntax you learned for strings in
Session 3. The first item is at index 0:
colors = ["red", "blue", "green", "yellow"]
print(colors[0]) # red
print(colors[1]) # blue
print(colors[-1]) # yellow (last)
Same indexing rules. Negative numbers count from the end.
How long is the list?
len() works on lists too:
colors = ["red", "blue", "green", "yellow"]
print(len(colors)) # 4
Adding to a list
The most common list operation: adding an item. Use the
.append() method:
colors = ["red", "blue"]
colors.append("green")
colors.append("yellow")
print(colors) # ['red', 'blue', 'green', 'yellow']
Each .append() adds one item to the end of the list.
Looping through a list
You can iterate through a list with for, just like you did
with strings:
colors = ["red", "blue", "green", "yellow"]
for color in colors:
print(f"I like the color {color}.")
Output:
I like the color red.
I like the color blue.
I like the color green.
I like the color yellow.
The loop runs once for each item. Each iteration, color (the
loop variable) holds the next item from the list.
This is the same syntax as for letter in name: from Session
3. Strings and lists both work with for ... in ....
Changing items
You can replace an item by assigning to its index:
colors = ["red", "blue", "green"]
colors[1] = "purple"
print(colors) # ['red', 'purple', 'green']
This is one of the things lists can do that strings can’t — strings are immutable (you can’t change their characters), but lists are mutable (you can).
Checkpoint: You’ve created a list, accessed items by
index, used .append() to add to it, and used a for loop to
print all the items. This is the natural stop point if class
is cut short.
Part B: A favorites collector
Time to use lists for something real.
What you’re building
A program that asks the user to enter their favorite things, one at a time. They keep entering until they type “done.” Then the program shows back the complete list.
Base goal
print("Favorites Collector!")
print("Enter your favorite things, one at a time. Type 'done' when finished.")
favorites = []
while True:
item = input("Favorite: ")
if item == "done":
break
favorites.append(item)
print(f"\nYou have {len(favorites)} favorites:")
for fav in favorites:
print(f" - {fav}")
Save. Run. Type a few favorites (pizza, soccer, blue, whatever). Type “done” when finished. The program shows the list.
Walk through what’s happening:
favorites = []— start with an empty list.- The
while True:loop keeps asking forever. - If the user types “done”, we
breakout of the loop. - Otherwise, we
.append()what they typed to the list. - After the loop ends,
len(favorites)and aforloop show the complete list.
That’s the base goal — a complete interactive program built around a list.
Stretch — number them
Right now the list shows:
- pizza
- soccer
- blue
Numbered would be nicer:
1. pizza
2. soccer
3. blue
Use range(len(favorites)) to get the indexes:
print(f"\nYou have {len(favorites)} favorites:")
for i in range(len(favorites)):
print(f" {i + 1}. {favorites[i]}")
The loop variable i goes 0, 1, 2… but we add 1 to display
“1, 2, 3…” (humans number from 1).
favorites[i] looks up the item at index i. Same
square-bracket syntax as accessing a single item.
Extension — categorize
Ask the user to categorize each favorite — food, color, activity, etc. Store both:
favorites = []
categories = []
while True:
item = input("Favorite (or 'done'): ")
if item == "done":
break
cat = input("What category? ")
favorites.append(item)
categories.append(cat)
print(f"\nYou have {len(favorites)} favorites:")
for i in range(len(favorites)):
print(f" {i + 1}. {favorites[i]} ({categories[i]})")
Two parallel lists. The favorite at index 0 has the category at index 0. They stay in sync because we always append to both at the same time.
(There are better ways to do this in Python — using a list of pairs, or a dictionary, or a list of dictionaries. We’ll get to dictionaries in Session 10. For now, two lists is fine.)
Extension — sorted output
Sort the list before showing it:
favorites.sort()
print(f"\nYour {len(favorites)} favorites (alphabetical):")
for fav in favorites:
print(f" - {fav}")
.sort() is a list method that sorts the list in place (it
changes the list itself). Strings sort alphabetically; numbers
sort numerically.
Wrap-up
Before we leave, share with the room:
- What’s in your favorites list? Anything funny?
- For the kids who built the categorized version — did the two parallel lists stay in sync?
- For the kids who used
.sort()— did the order surprise you? (Capital letters sort before lowercase letters.)
You learned today the most-used data structure in all of Python. Lists are everywhere. Files are read as lists of lines. Spreadsheet rows are lists of cells. Webpages are lists of elements. Programs that handle data, programs that handle collections of anything — they all use lists.
You also learned that a for loop knows how to walk through
a list (or a string). Same for X in Y: syntax, different
collections. From here on out, that pattern is your default
for “do something with each item.”
If you missed this session
Open Thonny and start a new file. Save as lists.py. Then:
-
Make a list and try basic operations:
colors = ["red", "blue", "green"] print(colors[0]) # red print(len(colors)) # 3 colors.append("yellow") print(colors) -
Loop through it:
for color in colors: print(f"I like {color}.") -
Build the favorites collector from Part B:
- Empty list
- while True loop
- input, check for “done”, append
- Print all at the end with a for loop
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
.remove(item)— removes the first occurrence of an item from the list.colors.remove("red")deletes “red”..pop()— removes and returns the last item.colors.pop()takes off the last one.infor membership —"red" in colorsreturns True/False. Sameinyou used forif letter in "aeiou":in Session 3.+between lists —[1, 2, 3] + [4, 5]is[1, 2, 3, 4, 5]. Combines two lists.*between lists and ints —["a"] * 5is["a", "a", "a", "a", "a"]. Useful for initialization.
What’s next
Next week we go deeper into lists — more methods, more
patterns, slicing (the [start:end] syntax you saw with
strings). After that, dictionaries — lists’ cousin that
holds named pairs instead of just ordered values.
Session 9: More lists — patterns and slicing
Session 9: More lists — patterns and slicing
Phase 3 — Python basics · Session 9 of 16
What we’re learning today
Last week you learned the basics of lists. Today we go deeper:
slicing (taking pieces of a list, just like with strings),
more useful list methods (sort, reverse, remove, pop), the
enumerate function (a cleaner way to loop with indexes),
and built-in functions like max, min, and sum
that work on any list. By the end of class, you’ll have built a
score tracker that does real statistics on a list of numbers.
You’ll need to remember from last time
- Lists —
[1, 2, 3], indexing with[0],len(),.append(). - Looping —
for item in colors:. - String slicing from Session 3 —
name[0:3]for the first three characters.
Part A: New ways to use lists
Open Thonny and start a new file. Save it as more_lists.py.
Slicing
Lists slice the same way strings do — same [start:end]
syntax:
numbers = [10, 20, 30, 40, 50, 60]
print(numbers[0:3]) # [10, 20, 30]
print(numbers[2:]) # [30, 40, 50, 60]
print(numbers[:3]) # [10, 20, 30]
print(numbers[-2:]) # [50, 60] — last two
print(numbers[::-1]) # [60, 50, 40, 30, 20, 10] — reversed
Same rules as string slicing: includes start, excludes
end. Negative numbers count from the end. [::-1] reverses.
The slice returns a new list — the original is unchanged.
enumerate — looping with indexes
Last week we used range(len(...)) to loop with index numbers:
for i in range(len(favorites)):
print(f"{i + 1}. {favorites[i]}")
That works, but Python has a cleaner way: enumerate.
for i, item in enumerate(favorites):
print(f"{i + 1}. {item}")
enumerate walks through a list and gives you both the index
and the item at the same time. Two variables (i and item)
both get values from each iteration.
This is shorter, cleaner, and what most Python programmers use.
max, min, sum
Some built-in functions work directly on lists of numbers:
scores = [85, 92, 78, 95, 88, 70]
print(max(scores)) # 95
print(min(scores)) # 70
print(sum(scores)) # 508
print(len(scores)) # 6
print(sum(scores) / len(scores)) # 84.66... (the average)
max returns the largest. min returns the smallest. sum
adds them all up. len you already know.
These are huge time-savers. Without them, you’d write a loop with a tracking variable and a conditional. With them — one function call.
More list methods
Beyond .append(), lists have several useful methods:
| Method | What it does |
|---|---|
.sort() | sorts the list in place (smallest to largest) |
.reverse() | reverses the list in place |
.remove(x) | removes the first occurrence of x |
.pop() | removes and returns the last item |
.pop(i) | removes and returns the item at index i |
.insert(i, x) | inserts x at index i (existing items shift right) |
.count(x) | counts how many times x appears |
.index(x) | returns the index of the first occurrence of x |
Try a few:
colors = ["red", "blue", "green", "blue"]
print(colors.count("blue")) # 2
print(colors.index("green")) # 2
colors.sort()
print(colors) # ['blue', 'blue', 'green', 'red']
colors.reverse()
print(colors) # ['red', 'green', 'blue', 'blue']
colors.remove("blue")
print(colors) # ['red', 'green', 'blue'] (only first removed)
last = colors.pop()
print(last) # 'blue'
print(colors) # ['red', 'green']
colors.insert(0, "purple")
print(colors) # ['purple', 'red', 'green']
Don’t memorize all of these. Refer back when you need one.
Checkpoint: You’ve used at least three of the new list operations from this section (slicing, enumerate, or one of the built-ins / methods). This is the natural stop point if class is cut short.
Part B: A score tracker
Time to build something that uses many of these together.
What you’re building
A program that asks the user to enter a series of test scores, then reports statistics: how many, the highest, the lowest, the average, and a sorted listing.
Base goal
print("Score Tracker")
print("Enter scores one at a time. Type 'done' when finished.")
scores = []
while True:
entry = input("Score: ")
if entry == "done":
break
scores.append(int(entry))
if len(scores) == 0:
print("No scores entered.")
else:
print(f"\nYou entered {len(scores)} scores:")
for i, score in enumerate(scores):
print(f" {i + 1}. {score}")
print(f"\nHighest: {max(scores)}")
print(f"Lowest: {min(scores)}")
print(f"Total: {sum(scores)}")
print(f"Average: {sum(scores) / len(scores):.1f}")
Save. Run. Type a few scores like 85, 92, 78, 95. Type
done. The program reports the statistics.
Walk through what’s happening:
scores = []— empty list.while True:loop withbreakon “done” — same pattern as the favorites collector.int(entry)converts the input string to a number before appending. Without this, themax/min/sumfunctions would either fail or behave weirdly with strings.if len(scores) == 0:handles the case where the user typed “done” immediately. (Always a good idea to check for empty data.)enumerate(scores)for numbered output.max,min,sum,lenfor the statistics.:.1fin the f-string for one decimal place on the average.
That’s the base goal. A complete program with input, processing, and statistics.
Stretch — show top three
Sort the scores and show the top three:
sorted_scores = sorted(scores, reverse=True)
print(f"\nTop three:")
for i, score in enumerate(sorted_scores[:3]):
print(f" {i + 1}. {score}")
What’s new:
sorted(scores, reverse=True)returns a new sorted list, highest to lowest. (Compare toscores.sort()which sorts in place.)sorted_scores[:3]slices the first three.enumeratenumbers them.
If you have fewer than 3 scores, the slice just returns whatever you have. (Lists tolerate slices that go past the end.)
Extension — a menu
Build a more interactive version with a menu:
print("Score Tracker")
scores = []
while True:
print("\nWhat would you like to do?")
print(" 1. Add a score")
print(" 2. Show all scores")
print(" 3. Show statistics")
print(" 4. Quit")
choice = input("Choice: ")
if choice == "1":
score = int(input("Score: "))
scores.append(score)
print(f"Added. You now have {len(scores)} scores.")
elif choice == "2":
if len(scores) == 0:
print("No scores yet.")
else:
for i, score in enumerate(scores):
print(f" {i + 1}. {score}")
elif choice == "3":
if len(scores) == 0:
print("No scores yet.")
else:
print(f" Highest: {max(scores)}")
print(f" Lowest: {min(scores)}")
print(f" Average: {sum(scores) / len(scores):.1f}")
elif choice == "4":
print("Goodbye!")
break
else:
print("Invalid choice. Try again.")
Now the user can add scores incrementally, view them, see statistics, and quit when they want. This is a menu-driven program — a common shape for command-line tools (which is exactly what Phase 4 is about).
Wrap-up
Before we leave, share with the room:
- For the kids who built the base — what scores did you enter? Did the average make sense?
- For the kids who did the menu version — does adding the menu make the program feel more like an app?
- Was anyone surprised by how much
max,min, andsumshortened the code?
You learned today the patterns that show up in every Python program that handles data. Lists, iteration, slicing, statistics functions, and the menu structure — these are the shapes that real programs are built from. Phase 4’s CLI tools will use all of them; Phase 8’s web apps will use them too.
You also met sorted(...) (returns a new sorted list) vs
scores.sort() (sorts in place). The choice between “modify
the original” and “give me a new one” comes up a lot in Python
— for lists, dicts, and other data types. Knowing the
difference saves real bugs.
If you missed this session
Open Thonny and start a new file. Save as more_lists.py.
Then:
-
Try slicing on a list:
numbers = [10, 20, 30, 40, 50] print(numbers[0:3]) print(numbers[::-1]) -
Use
enumerate:colors = ["red", "blue", "green"] for i, color in enumerate(colors): print(f"{i}: {color}") -
Try
max,min,sum:scores = [85, 92, 78, 95] print(max(scores)) print(sum(scores) / len(scores)) -
Build the score tracker base from Part B.
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
sum(scores) / len(scores)is the average, but Python’sstatisticsmodule hasstatistics.mean(scores)—import statisticsand try it.sorted(scores)gives ascending;sorted(scores, reverse=True)gives descending. Same for.sort(reverse=True).- List comprehensions — Python’s slick way to build a list
from another.
doubled = [x * 2 for x in scores]makes a new list with every score doubled. We won’t formally cover; try it if you’re curious. - Nested lists — a list of lists.
grid = [[1, 2, 3], [4, 5, 6]]. Access withgrid[0][1](returns 2). Useful for 2D data like game boards.
What’s next
Next week we meet dictionaries — like lists, but instead of positions (0, 1, 2…) you use names to look up values (name → phone number, country → capital, word → definition). Dictionaries solve the parallel-lists problem from Session 8 and unlock a whole new class of programs.
Session 10: Dictionaries — key-value pairs
Session 10: Dictionaries — key-value pairs
Phase 3 — Python basics · Session 10 of 16
What we’re learning today
Lists hold values in order: the first one, the second one, the third. Sometimes you need a different kind of organization — “this name has this phone number,” “this country has this capital,” “this word has this definition.” That’s a dictionary, the second-most-important data structure in Python after lists. By the end of class, you’ll have built a contact book that stores names with phone numbers and lets you look them up.
You’ll need to remember from last time
- Lists —
[1, 2, 3], indexing, iteration. enumeratefor looping with index.max,min,sumfor stats.while True:andbreakfor “until done” loops.- The
inoperator —"red" in colorsreturns True/False.
Part A: What dictionaries are and how to use them
Open Thonny and start a new file. Save it as dicts.py.
The motivation
Remember Session 8’s “favorites with categories” extension? We had two parallel lists:
favorites = ["pizza", "soccer", "blue"]
categories = ["food", "activity", "color"]
Items at the same index belong together. Item 0 was “pizza” which is a “food”. This works but it’s awkward — if you remove something from one list, you have to remember to remove from the other too. The lists can get out of sync.
A dictionary solves this. Instead of two lists with matching indexes, you have ONE structure where each value has a name attached:
favorites = {
"pizza": "food",
"soccer": "activity",
"blue": "color"
}
The names (“pizza”, “soccer”, “blue”) are called keys. The things they point to (“food”, “activity”, “color”) are called values. Together they’re key-value pairs.
You look up a value by its key:
print(favorites["pizza"]) # food
print(favorites["soccer"]) # activity
Same [ ] syntax as lists — but instead of a number for the
index, you use the key.
Making a dictionary
The syntax uses curly braces with key: value pairs:
person = {
"name": "Sam",
"age": 12,
"color": "blue"
}
Three keys ("name", "age", "color"), three values. The
curly braces and the colons are required.
You can also start with an empty dictionary and add to it:
person = {}
person["name"] = "Sam"
person["age"] = 12
print(person) # {'name': 'Sam', 'age': 12}
The same [key] = value syntax adds new entries (if the key
doesn’t exist) or updates existing ones (if it does).
Looking up values
Two ways:
person = {"name": "Sam", "age": 12}
print(person["name"]) # Sam
Direct lookup with [key]. Simple, but if the key doesn’t
exist, you get a KeyError:
print(person["color"]) # KeyError: 'color'
Safer way: check first with in:
if "color" in person:
print(person["color"])
else:
print("No color set.")
The in operator returns True/False — same in you’ve used
with strings and lists, just checking if a key exists.
Iterating
Two common ways to loop through a dictionary:
person = {"name": "Sam", "age": 12, "color": "blue"}
# loop through keys
for key in person:
print(key) # name, age, color (one per line)
# loop through key-value pairs
for key, value in person.items():
print(f"{key}: {value}")
The first form gives you each key in turn (one variable). The
second uses .items() and gives you both key AND value at
the same time (two variables, like enumerate from Session 9).
The second form is what you’ll use most often.
How big is the dictionary?
len() works on dicts too — it tells you how many key-value
pairs:
person = {"name": "Sam", "age": 12}
print(len(person)) # 2
Checkpoint: You’ve created a dictionary, looked up at least
one value by key, used in to check whether a key exists, and
iterated through it with .items(). This is the natural
stop point if class is cut short.
Part B: A contact book
Time to use dictionaries for something practical.
What you’re building
A program that stores phone numbers by name. The user can add contacts, look up a name, and view all contacts.
Base goal
print("Contact Book")
contacts = {}
while True:
print("\nWhat do you want to do?")
print(" 1. Add a contact")
print(" 2. Look up a contact")
print(" 3. Show all contacts")
print(" 4. Quit")
choice = input("Choice: ")
if choice == "1":
name = input("Name: ")
phone = input("Phone: ")
contacts[name] = phone
print(f"Added {name}.")
elif choice == "2":
name = input("Name to look up: ")
if name in contacts:
print(f"{name}'s phone: {contacts[name]}")
else:
print(f"{name} is not in your contacts.")
elif choice == "3":
if len(contacts) == 0:
print("No contacts yet.")
else:
print(f"\nYour {len(contacts)} contacts:")
for name, phone in contacts.items():
print(f" {name}: {phone}")
elif choice == "4":
print("Goodbye!")
break
else:
print("Invalid choice.")
Save. Run. Add a few contacts. Look one up. Show all. Quit.
Walk through what’s happening:
contacts = {}— empty dict to start.- Menu loop (similar to last week’s score tracker).
- Add:
contacts[name] = phoneadds a new pair (or updates an existing one). - Look up:
if name in contacts:checks for the key first (avoids KeyError); thencontacts[name]gets the phone. - Show all:
contacts.items()for iterating both keys and values together.
That’s the base goal. A working contact book using all the dictionary basics.
Stretch — remove contact
Add an option to delete a contact:
elif choice == "5":
name = input("Name to delete: ")
if name in contacts:
del contacts[name]
print(f"Deleted {name}.")
else:
print(f"{name} is not in your contacts.")
(Add print(" 5. Delete a contact") to the menu, of course.)
The del keyword removes a key (and its value) from the dict.
del contacts[name] removes the entry for that name.
Extension — a word counter
Different application of dicts: counting how many times each word appears in a sentence.
print("Word Counter")
sentence = input("Enter a sentence: ")
words = sentence.lower().split()
counts = {}
for word in words:
if word in counts:
counts[word] = counts[word] + 1
else:
counts[word] = 1
print(f"\nWord counts:")
for word, count in counts.items():
print(f" {word}: {count}")
What’s new:
sentence.lower().split()— convert to lowercase, then split into a list of words. (.split()with no argument splits on whitespace.)- The
for word in words:loop checks each word. - If the word is already in
counts, add 1 to its current count. Otherwise, set it to 1 (first time we’ve seen it).
Try it with a sentence like “the quick brown fox jumps over the lazy dog the fox sees the dog” — you should see “the” appear 4 times, “fox” 2 times, “dog” 2 times, and most other words 1 time.
This pattern (counting things into a dict) is one of the most common Python idioms. Once you have the shape, you can count anything: characters in a string, scores in a list, items in any collection.
Wrap-up
Before we leave, share with the room:
- For the kids who built the contact book — did the menu feel natural?
- For the kids who did the word counter — did anything in your sentence appear more often than you expected?
- Can anyone think of another situation where a dictionary would be useful? (Examples: a quiz program with question-answer pairs, a translation tool with word→translation, a game with player→score, etc.)
You learned today the second-most-important Python data structure. Dictionaries are everywhere in real programs — configuration files, JSON data from web APIs, database results, HTTP request data. Anywhere there’s “this thing has this value” information, dictionaries are the tool.
You also learned a key Python pattern: count-into-a-dict. Walk through a collection, increment the count for each item. This pattern shows up in word frequency, vote counting, error counting, anywhere you need to tally things by category.
If you missed this session
Open Thonny and start a new file. Save as dicts.py. Then:
-
Make a dictionary and look up values:
person = {"name": "Sam", "age": 12} print(person["name"]) print(person["age"]) -
Add and update keys:
person["color"] = "blue" person["age"] = 13 # update print(person) -
Iterate:
for key, value in person.items(): print(f"{key}: {value}") -
Build the contact book from Part B (menu + add + look up + show all).
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
dict.keys()returns a view of just the keys;dict.values()returns just the values. Useful in different situations.dict.get(key, default)— likedict[key]but returnsdefaultif the key isn’t there (instead of raising KeyError). For example:counts.get("hello", 0)returns 0 if “hello” isn’t a key.- Nested dicts — values can be dicts themselves.
people = {"sam": {"age": 12, "color": "blue"}}. Useful for more complex data. - Dict comprehensions — like list comprehensions but for
dicts.
squares = {n: n*n for n in range(5)}. Concise but more advanced; try if curious.
What’s next
Next week we learn how to read data from files — your
contact book that disappears when you close the program is
about to become one that remembers contacts between sessions.
That’s open() and file reading, the first step toward
programs that work with real-world data.
Session 11: Reading and writing files
Session 11: Reading and writing files
Phase 3 — Python basics · Session 11 of 16
What we’re learning today
So far, every program you’ve written forgets everything the moment it ends. Type some favorites, close the program, gone. Today we fix that. We’ll learn how to write information to a file on your computer and read it back later. By the end of class, your contact book (or any program with a list) can remember information between runs.
You’ll need to remember from last time
- Lists and dictionaries — the things we’ll save and load.
forloops for iterating through collections.while True:andbreakfor input loops..strip()from Session 3 — useful for cleaning up text from files.- String concatenation and f-strings for building text to write.
Part A: Writing a file
Open Thonny and start a new file. Save it as files.py.
with open(...) — the safe way
To work with a file, Python uses the open() function. The
modern, safe way to use it is with a with block:
with open("hello.txt", "w") as f:
f.write("Hello, file!\n")
Save and run.
Then look in the same folder where files.py is saved. There
should be a new file: hello.txt. Open it (in Thonny, or any
text editor). It contains:
Hello, file!
You just wrote a file. Walk through the code:
with open("hello.txt", "w") as f:— opens a file. The"w"means write mode (creates the file if it doesn’t exist; replaces it if it does). The file is given the temporary namefinside the block.f.write("Hello, file!\n")— writes a string to the file. The\nat the end is a newline (line break), so the next thing written goes on a new line.- The
withblock automatically closes the file when it ends. That’s the magic ofwith— you don’t have to remember to clean up.
Three modes
The second argument to open() is the mode:
| Mode | What it does |
|---|---|
"r" | read — file must exist; you can read but not write |
"w" | write — replaces the file (or creates it) |
"a" | append — adds to the end of an existing file |
Use "w" carefully — it erases whatever was there. "a" is
safer if you want to add to a file without losing what’s there.
Writing multiple lines
Each .write() writes exactly what you give it. Newlines
(\n) are how you separate lines:
with open("favorites.txt", "w") as f:
f.write("pizza\n")
f.write("soccer\n")
f.write("blue\n")
Run. Open favorites.txt. Three lines.
You can also use a loop to write a whole list:
favorites = ["pizza", "soccer", "blue", "books"]
with open("favorites.txt", "w") as f:
for fav in favorites:
f.write(fav + "\n")
Same result, but driven by a list. This is the standard pattern for saving a list to a file.
Checkpoint: You’ve written at least one file using
with open(..., "w") as f: and .write(...). This is the
natural stop point if class is cut short.
Part B: Reading a file (and a real project)
Now let’s read the file back.
Reading line by line
The most common pattern: loop through the lines.
with open("favorites.txt", "r") as f:
for line in f:
print(line)
Run.
pizza
soccer
blue
books
Notice the extra blank lines. That’s because each line in
the file ends with \n, AND print adds another \n at the
end. Two newlines = blank line.
Fix it with .strip():
with open("favorites.txt", "r") as f:
for line in f:
clean = line.strip()
print(clean)
.strip() removes whitespace (including newlines) from the
start and end of a string. Now the print adds the only
newline, and the output looks right.
This is one of the most common file-reading patterns: open,
loop with for line in f:, strip each line.
Reading into a list
Sometimes you want all the lines in a list (so you can shuffle them, sort them, etc.):
with open("favorites.txt", "r") as f:
favorites = []
for line in f:
favorites.append(line.strip())
print(favorites)
print(f"You have {len(favorites)} favorites.")
Now favorites is a list of strings, ready to use just like
any other list.
A persistent notes program
Build something that uses both reading AND writing. A simple notes-keeper that remembers your notes between runs:
NOTES_FILE = "notes.txt"
# Load existing notes
notes = []
try:
with open(NOTES_FILE, "r") as f:
for line in f:
notes.append(line.strip())
except FileNotFoundError:
pass # no file yet, that's fine — empty list
print(f"You have {len(notes)} notes.")
for i, note in enumerate(notes):
print(f" {i + 1}. {note}")
# Add new notes
print("\nAdd more notes (or 'done' to save and exit):")
while True:
note = input("Note: ")
if note == "done":
break
notes.append(note)
# Save everything back
with open(NOTES_FILE, "w") as f:
for note in notes:
f.write(note + "\n")
print(f"\nSaved {len(notes)} notes to {NOTES_FILE}.")
Save. Run. Add a few notes. Quit. Run again — your previous notes should still be there. Add more. Quit. Run a third time — all your notes from all three runs are there.
Walk through what’s new:
NOTES_FILE = "notes.txt"— the filename in a constant at the top. Easy to change in one place.try: ... except FileNotFoundError: pass— try to load the file, but if it doesn’t exist (first run), just skip the loading step. (This is a sneak peek of error handling — full treatment in Session 13.)- The rest is patterns from Sessions 8-10 (lists, append, enumerate, while loop) plus today’s file I/O.
That’s the base goal — a real persistent program.
Stretch — append mode
The base version overwrites the file every time. That works, but you read everything into memory and write it all back. For large files this is wasteful.
Append mode ("a") just adds to the end:
with open("log.txt", "a") as f:
f.write("Program ran on Wednesday.\n")
Each run appends a new line, without touching what was already there. Useful for logs.
Try writing a “diary” program that asks for one entry and
appends it to a diary.txt file with a timestamp. Each run
adds one more entry; the file grows over time.
Extension — a file processor
Take a real file and process it. If your machine has a .txt
file with some content already (a story, your notes, anything),
write a program that reads it and reports:
- Total number of lines
- Total number of words
- Total number of characters
- The longest line (and how long)
filename = input("Filename: ")
with open(filename, "r") as f:
lines = []
for line in f:
lines.append(line.strip())
print(f"\nFile: {filename}")
print(f"Lines: {len(lines)}")
total_words = 0
total_chars = 0
longest_line = ""
for line in lines:
words = line.split()
total_words = total_words + len(words)
total_chars = total_chars + len(line)
if len(line) > len(longest_line):
longest_line = line
print(f"Words: {total_words}")
print(f"Characters: {total_chars}")
print(f"Longest line ({len(longest_line)} chars): {longest_line[:60]}...")
This pulls together file I/O, lists, iteration, max-tracking, slicing, f-strings — basically all of Phase 3 so far in one program.
Wrap-up
Before we leave, share with the room:
- For the kids who built the notes program — does it actually remember between runs? (Run it, add a note, quit. Run it again. Note still there.)
- For the kids who tried append mode — what’s the difference between writing each time vs appending?
- What other programs would benefit from saving to a file? (High scores in a game! Settings! Saved progress!)
You learned today how to make a program remember between runs. This is a huge step. Until today, your programs lived only while they ran. Now they can persist — store data, load it back, modify it over time. Almost every real program does this: documents save to files, games save progress to files, settings save to files.
You also got a taste of try/except for the case where the
file doesn’t exist yet. Next week we’ll cover error handling
properly.
If you missed this session
Open Thonny and start a new file. Save as files.py. Then:
-
Write a file:
with open("hello.txt", "w") as f: f.write("Hello, file!\n")Save, run. Look for
hello.txtin the same folder. -
Read it back:
with open("hello.txt", "r") as f: for line in f: print(line.strip()) -
Build the persistent notes program from Part B.
About 35-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
f.read()reads the entire file as one string. Useful for small files where you want the whole thing at once.f.readlines()reads all lines into a list (with the\nstill attached to each — usually you.strip()after).- CSV files — comma-separated values, the format of
spreadsheets. Python has a
csvmodule. We’ll touch this in Session 14. - Different file locations — by default,
open("name.txt")uses the current folder. You can also use absolute paths like"/home/sam/notes.txt"(on Linux) or relative paths like"data/notes.txt".
What’s next
Next week is hangman — your biggest Phase 3 project so far. You’ll build a complete word-guessing game using lists, dictionaries, loops, conditionals, functions, AND today’s file I/O (the word list comes from a file). It pulls together everything you’ve learned.
Session 12: Hangman — a complete game
Session 12: Hangman — a complete game
Phase 3 — Python basics · Session 12 of 16
What we’re learning today
Today we build the biggest Phase 3 project so far — a complete game of hangman. The computer picks a secret word; you guess letters one at a time; if you guess too many wrong, you lose. By the end of class, you’ll have a working game that uses every idea you’ve learned in Phase 3 — input/output, strings, lists, conditionals, loops, functions, file I/O.
You’ll need to remember from last time
- Lists —
[],.append(), indexing,in. - Strings — indexing,
for letter in word:,.lower(). while True:andbreakfor game loops.- Functions with return values — Session 6.
import randomandrandom.choice(...)— picks a random item from a list.with open(filename) as f:— reading from a file.
Part A: Build the display
Open Thonny and start a new file. Save it as hangman.py.
The plan
Hangman:
- The computer picks a secret word.
- The user sees the word with
_for letters they haven’t guessed. - The user guesses one letter at a time.
- If the letter is in the word, fill in the blanks.
- If not, count it as a wrong guess.
- Win when all letters are filled in. Lose after too many wrong guesses.
We’ll build it in steps.
Step 1: Pick a word
import random
words = ["python", "scratch", "turtle", "function", "variable", "computer"]
secret = random.choice(words)
print(f"DEBUG: secret is {secret}")
random.choice(words) picks one item from the list at random.
We’re printing the secret while developing — we’ll remove it
later, like in Session 7.
Run a few times. Each time, a different secret word.
Step 2: Show the word with blanks
The display should show one _ for each letter that hasn’t
been guessed. Right now, no letters have been guessed. So:
import random
words = ["python", "scratch", "turtle", "function", "variable", "computer"]
secret = random.choice(words)
guessed_letters = []
# build the display
display = ""
for letter in secret:
if letter in guessed_letters:
display = display + letter
else:
display = display + "_"
print(f"Word: {display}")
Walk through:
guessed_letters = []— empty list of letters guessed so far (none yet).- For each letter in the secret word, if it’s been guessed,
show it. Otherwise show
_. - Build up the
displaystring letter by letter.
Run. The display should be all underscores — like ______ for
“python.”
Step 3: Make it a function
The “build the display” code will run multiple times (every time the user guesses). Pull it into a function:
def make_display(secret, guessed_letters):
display = ""
for letter in secret:
if letter in guessed_letters:
display = display + letter
else:
display = display + "_"
return display
# test it
print(make_display("python", [])) # ______
print(make_display("python", ["p", "n"])) # p____n
print(make_display("python", ["p", "y", "t", "h", "o", "n"])) # python
Now make_display is a reusable function that takes the secret
and the list of guessed letters, and returns the display
string. Test it with different inputs to be sure.
Checkpoint: You’ve built a make_display function and
tested it with different combinations of secret words and
guessed letters. This is the natural stop point if class is
cut short.
Part B: The full game loop
Now we add input, the game loop, win and lose conditions.
Step 4: The game loop
import random
def make_display(secret, guessed_letters):
display = ""
for letter in secret:
if letter in guessed_letters:
display = display + letter
else:
display = display + "_"
return display
# setup
words = ["python", "scratch", "turtle", "function", "variable", "computer"]
secret = random.choice(words)
guessed_letters = []
wrong_count = 0
max_wrong = 6
# game loop
while True:
display = make_display(secret, guessed_letters)
print(f"\nWord: {display}")
print(f"Guessed: {guessed_letters}")
print(f"Wrong: {wrong_count}/{max_wrong}")
# win check
if display == secret:
print(f"You won! The word was {secret}.")
break
# lose check
if wrong_count >= max_wrong:
print(f"You lose! The word was {secret}.")
break
# get a guess
guess = input("Guess a letter: ").lower()
if guess in guessed_letters:
print("You already guessed that letter.")
continue
guessed_letters.append(guess)
if guess in secret:
print("Good guess!")
else:
wrong_count = wrong_count + 1
print("Wrong!")
Save. Run. Play.
Walk through what’s new:
wrong_count = 0andmax_wrong = 6— track how many wrong guesses, with a limit.- The
while True:loop withbreakfor both win and lose. display = make_display(...)recomputes each loop.- Win check:
if display == secret:— once the display has no more underscores, all letters are guessed. - Lose check:
if wrong_count >= max_wrong:— used too many guesses. - Get a guess:
.lower()to make the comparison case-insensitive. - Already guessed: check before adding;
continueskips the rest of this iteration and goes back to the top of the loop. - Track wrong guesses: only increment if the letter isn’t in the word.
That’s the base game — a complete, playable hangman.
Step 5: Words from a file
Right now the word list is hardcoded. Let’s load it from a file (callback to Session 11!).
First, create a file words.txt (in the same folder as
hangman.py) with a few words, one per line:
python
scratch
turtle
function
variable
computer
keyboard
monitor
(Actually create this file in Thonny, save as words.txt. Or
write a quick one-time program to create it.)
Then in hangman.py, replace the hardcoded list:
# load words from file
words = []
with open("words.txt", "r") as f:
for line in f:
word = line.strip()
if word: # skip blank lines
words.append(word)
secret = random.choice(words)
Now the word list lives in words.txt. To add or change words,
edit the file — no need to touch the Python code.
This is the base + file enhancement. Most real games and apps work this way — the data lives in files, the code lives in code, they’re separate.
Stretch — show the wrong letters
Currently, guessed_letters shows ALL letters guessed (right
and wrong mixed together). Show just the wrong ones for
clarity:
wrong_letters = []
# inside the loop, replace the wrong-guess block:
if guess in secret:
print("Good guess!")
else:
wrong_count = wrong_count + 1
wrong_letters.append(guess)
print("Wrong!")
# update the display each round:
print(f"Wrong letters: {wrong_letters}")
Two lists now — one for all guessed (to avoid duplicates), one for just the wrong ones (for display).
Extension — visual hangman
Add ASCII art that shows the “hangman” stick figure progressing as wrong guesses pile up. Build a function that returns the right picture for the wrong count:
def make_hangman(wrong_count):
if wrong_count == 0:
return ""
elif wrong_count == 1:
return " O"
elif wrong_count == 2:
return " O\n |"
elif wrong_count == 3:
return " O\n /|"
elif wrong_count == 4:
return " O\n /|\\"
elif wrong_count == 5:
return " O\n /|\\\n /"
else:
return " O\n /|\\\n / \\"
# call it inside the loop:
print(make_hangman(wrong_count))
The \n makes the picture span multiple lines. The \\ is an
escaped backslash. (To print one \, you write \\ in the
string.)
Extension++ — play again loop
Wrap the whole game in another loop so the user can play multiple times (callback to Session 7’s number-guessing game).
while True:
# ... entire game ...
again = input("\nPlay again? (y/n): ")
if again.lower() != "y":
print("Thanks for playing!")
break
Wrap-up
Before we leave, share with the room:
- For the kids who finished the base game — what’s the hardest word you’ve had?
- For the kids who added words to the file — how many do you have?
- For the visual-hangman extension — does the figure look right?
You built a complete game today. Hangman uses every concept from Phase 3 so far — print, input, types, strings, conditionals, loops, functions, lists, and (with the file extension) file I/O. That’s not nothing.
You also encountered a key real-world pattern: data and code separated. The words list lives in a text file; the game logic lives in Python. To change the words, you don’t touch the code. This separation is how almost all real software is organized.
If you missed this session
Open Thonny and start a new file. Save as hangman.py. Then:
-
Build the
make_display(secret, guessed_letters)function from Part A. Test it with a few inputs. -
Add the game loop from Part B (Step 4) —
while True:, win check, lose check, get guess, track wrong count. -
Play it. Get the hardcoded version working before trying to load words from a file.
-
Optionally: create a
words.txtfile with a list of words and load it (Step 5).
About 50-60 minutes — this is a long session. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Difficulty levels — different word lists for easy/medium/
hard, or different
max_wrongvalues. - Categories — words organized by category (animals, colors, programming terms). User picks a category.
- Two-player mode — one player enters a word, the other guesses. (Tricky: hide the word as it’s typed.)
- Save high scores to a file — fewest wrong guesses, best word completion, etc.
What’s next
Next week we learn error handling — try/except for
making programs that don’t crash when the user types something
unexpected. You’ve already seen one try/except in Session
11; next week we’ll cover it properly and use it to make your
programs robust.
Session 13: Error handling — try and except
Session 13: Error handling — try and except
Phase 3 — Python basics · Session 13 of 16
What we’re learning today
When something goes wrong in your program — the user types
“hello” when you wanted a number, a file doesn’t exist, you
divide by zero — Python crashes by default. Today we
learn to catch those errors and handle them gracefully
with try and except. By the end of class, you’ll be able
to write programs that don’t crash on bad input — they ask
again instead.
You’ll need to remember from last time
int(input(...))— converts user input to a number, but crashes if it’s not numeric.open(...)— fails if the file doesn’t exist.- Function definitions — Sessions 4 and 6.
while True:andbreak— for “keep trying” loops.
Part A: try and except
Open Thonny and start a new file. Save it as try_except.py.
The problem
Try this:
age = int(input("How old are you? "))
print(f"In ten years you'll be {age + 10}.")
Run. Type your age. Works fine.
Now run again. This time, type “hello” instead of a number.
ValueError: invalid literal for int() with base 10: 'hello'
Crash. Your program ends. The user — who maybe just made a typo — sees an error message that looks scary and confusing.
Real programs don’t do that. They handle the error and ask again.
try and except
Here’s how you handle it:
try:
age = int(input("How old are you? "))
print(f"In ten years you'll be {age + 10}.")
except ValueError:
print("That's not a number!")
Save and run. Type “hello.” Now you get:
That's not a number!
No crash. No scary message. The program ends cleanly with a friendly note.
Walk through:
try:— start a block of code that might fail.- The code inside the
tryruns as normal. - If an error happens, Python jumps to the
exceptblock matching the error type. - If no error, the
exceptblock is skipped.
The error type after except (ValueError here) tells Python
which kinds of errors to catch. ValueError is the error
int() raises when its input can’t be converted to a number.
Common exception types
Python has many built-in exception types. Some common ones:
| Exception | When it happens |
|---|---|
ValueError | bad value given to a function (int("hello")) |
ZeroDivisionError | dividing by zero |
FileNotFoundError | trying to open a file that doesn’t exist |
KeyError | dict lookup with a missing key |
IndexError | list lookup with an out-of-range index |
TypeError | wrong type passed ("5" + 3) |
NameError | using a variable that doesn’t exist |
Try a few:
# ZeroDivisionError
try:
print(10 / 0)
except ZeroDivisionError:
print("Can't divide by zero!")
# KeyError
person = {"name": "Sam"}
try:
print(person["age"])
except KeyError:
print("No age stored.")
Each try runs the risky code; each matching except
catches the specific failure.
Why catch specific exceptions
You can catch any error with a bare except: — but this is
usually a bad idea:
try:
age = int(input("Age? "))
except:
print("Something went wrong.") # what went wrong??
This catches everything — including bugs in your own code, which you probably wanted to know about. Always catch the specific exception you’re handling:
try:
age = int(input("Age? "))
except ValueError:
print("That's not a number!")
Now the only error caught is the one you expected. Other bugs still crash, which is what you want during development.
Try-again loop
The most common pattern: ask the user, try to convert, if it fails ask again:
while True:
try:
age = int(input("How old are you? "))
break # success — exit the loop
except ValueError:
print("That's not a number. Try again.")
print(f"You're {age} years old.")
Walk through:
while True:— loop forever (until break).- Inside the
try: ask, convert, if successfulbreakout. - Inside the
except: print error, loop continues.
The break is inside the try because we want to break
only on success. If the int() fails, the break is skipped
and the except block runs instead.
This is a very common Python pattern. Memorize the shape.
Checkpoint: You’ve used try / except to catch at least
one specific exception, and you’ve written a try-again loop
that keeps asking until the user provides valid input. This
is the natural stop point if class is cut short.
Part B: A robust input function
Time to use what you learned to make your previous programs better.
The function
Build a reusable function that asks for a number and won’t take no for an answer:
def ask_for_number(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("That's not a number. Try again.")
Walk through:
- A function with a parameter (
prompt). - Infinite loop.
- Try to convert; if successful, return the number immediately (which exits the function and the loop).
- If conversion fails, print the message and loop back.
Now use it:
def ask_for_number(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("That's not a number. Try again.")
age = ask_for_number("How old are you? ")
years = ask_for_number("How many years from now? ")
future_age = age + years
print(f"In {years} years, you'll be {future_age}.")
Run. Type bad input. The function asks again. Type good input. Continue.
This pattern — wrap risky input in a function with try-again logic — is one of the most useful tools you’ll have.
That’s the base goal.
Stretch — bounded numbers
Often you want a number in a range. Modify the function:
def ask_for_number(prompt, min_value, max_value):
while True:
try:
n = int(input(prompt))
if n < min_value or n > max_value:
print(f"Number must be between {min_value} and {max_value}.")
continue
return n
except ValueError:
print("That's not a number. Try again.")
# usage:
age = ask_for_number("How old are you? ", 0, 120)
Now the function rejects out-of-range numbers too. The
continue skips the return and goes back to the top of the
loop.
Extension — apply to your hangman game
Open last week’s hangman game (or rebuild it). Add error handling for these cases:
- User types nothing (just Enter): say “Please type a letter.”
- User types more than one character: say “One letter at a time.”
- User types something that isn’t a letter (a number, a symbol): say “Letters only.”
This doesn’t strictly need try/except — most of these are
just if checks. But the spirit is the same: don’t crash;
ask again.
def ask_for_letter():
while True:
guess = input("Guess a letter: ").lower()
if len(guess) == 0:
print("Please type a letter.")
elif len(guess) > 1:
print("One letter at a time.")
elif not guess.isalpha():
print("Letters only.")
else:
return guess
guess.isalpha() returns True if the string is all letters.
Useful string method, similar to the .lower()/.upper() etc.
from Session 3.
Replace the input("Guess a letter: ").lower() line in your
hangman with guess = ask_for_letter(). Now your game is
much more robust.
Extension — file-not-found
Update your persistent notes program from Session 11 to use
a proper try/except:
notes = []
try:
with open(NOTES_FILE, "r") as f:
for line in f:
notes.append(line.strip())
except FileNotFoundError:
print("No notes file yet — starting fresh.")
Same pattern, different exception type. Now the program tells the user explicitly what’s happening on first run, instead of silently doing nothing.
Wrap-up
Before we leave, share with the room:
- For the kids who built
ask_for_number— does it feel satisfying to type bad input and have the program just ask again? - For the kids who used it in hangman — what edge cases did you handle?
- Think back to all the programs you’ve written this phase.
Which ones would have benefited from
try/except? (Most of them.)
You learned today how to write programs that don’t crash on bad input. This is the difference between code that technically works and code that’s actually usable. Real software handles errors everywhere — invalid user input, missing files, network failures, bad data, you name it. Today is your introduction to a habit you’ll use for the rest of your programming life.
You also learned the try-again loop pattern —
while True: try: ... return ... except: print error. This is
the canonical Python idiom for “keep asking until the user
gives me what I need.” You’ll use it constantly.
If you missed this session
Open Thonny and start a new file. Save as try_except.py.
Then:
-
Try the basic try/except:
try: age = int(input("Age? ")) print(f"You're {age}.") except ValueError: print("That's not a number.")Run, try good input. Run again, try bad input. See the difference.
-
Build the try-again loop pattern from Part A.
-
Build the
ask_for_numberfunction from Part B and use it in a small program.
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
elseclause —try / except / else:. Theelseruns only if no exception happened. Useful for separating “the risky code” from “the code that depends on it succeeding.”finallyclause —try / except / finally:. Thefinallyalways runs, regardless of exceptions. Useful for cleanup.- Catching multiple exception types:
except (ValueError, TypeError):— catches either. - Catching the exception itself:
except ValueError as e:—eis the exception object, with details about what went wrong. raise ValueError("custom message")— make your own errors when something is wrong in your code’s logic.
What’s next
Next week is the biggest integration project of Phase 3 — either a text adventure game (multiple rooms, choices that change the story) or a CSV reader (a real-world data tool). You’ll use everything you’ve learned, including today’s error handling, to build something genuinely impressive.
Session 14: Putting it together — a text adventure
Session 14: Putting it together — a text adventure
Phase 3 — Python basics · Session 14 of 16
What we’re learning today
Today we build the biggest project of Phase 3 — a text adventure game. The player walks through a small world (rooms connected by exits), reads descriptions, types commands like “north” or “look”, and tries to find their way to the goal. Everything from Phase 3 comes together: dictionaries (for the rooms), loops (the game loop), conditionals (handling commands), strings, input/output, error handling, and optionally file I/O. By the end of class, you’ll have a playable adventure of your own design.
You’ll need to remember from last time
- Dictionaries —
{"key": "value"}, lookup with[key], theinoperator,.items(). - Lists — for inventories or anything ordered.
while True:andbreakfor game loops.if/elif/elsefor handling different commands.- f-strings for output.
try/exceptfor handling unexpected input.
Part A: Build the world
Open Thonny and start a new file. Save it as adventure.py.
The plan
A text adventure has:
- Rooms — places the player can be. Each one has a description and a list of exits to other rooms.
- A current location — which room the player is in right now.
- A game loop — print the room, ask the user what to do, update the location.
- A win condition — reach a specific room.
We’ll use a dictionary of rooms — perfect application of what we learned in Session 10.
Build the rooms
rooms = {
"entry": {
"description": "You're in a dark entryway. Doors lead north and east.",
"north": "kitchen",
"east": "library"
},
"kitchen": {
"description": "A bright kitchen. The entryway is south.",
"south": "entry"
},
"library": {
"description": "Books from floor to ceiling. The entryway is west. Stairs go up.",
"west": "entry",
"up": "study"
},
"study": {
"description": "A quiet study. You found the treasure chest!",
"down": "library"
}
}
This is a dictionary of dictionaries. The outer dict has
room names as keys. Each value is another dict describing
that room — its "description" and its exits ("north",
"east", etc.) which point to other room names.
So if you’re in "entry" and you go "north", you end up in
"kitchen".
Show the current room
current = "entry"
room = rooms[current]
print(room["description"])
rooms[current] looks up the current room by name. Then
room["description"] gets its description string. Two dict
lookups in a row.
Save and run. You should see:
You're in a dark entryway. Doors lead north and east.
The game loop
Now the loop: print the room, ask for a command, update the location, repeat.
rooms = {
"entry": {
"description": "You're in a dark entryway. Doors lead north and east.",
"north": "kitchen",
"east": "library"
},
"kitchen": {
"description": "A bright kitchen. The entryway is south.",
"south": "entry"
},
"library": {
"description": "Books from floor to ceiling. The entryway is west. Stairs go up.",
"west": "entry",
"up": "study"
},
"study": {
"description": "A quiet study. You found the treasure chest!",
"down": "library"
}
}
current = "entry"
while True:
room = rooms[current]
print(f"\n{room['description']}")
# win check
if current == "study":
print("You won!")
break
# get a command
command = input("> ").lower().strip()
if command == "quit":
print("Goodbye!")
break
elif command in room:
current = room[command]
else:
print("You can't go that way.")
Save. Run. Type commands like north, east, up, quit.
Try to find the study (the win room).
Walk through what’s happening:
current = "entry"— start in the entry room.while True:— game loops forever (until break).- Each iteration: look up the current room, print its description, check for win.
- Get a command. Commands are direction names (
north,east, etc.) orquit. if command in room:— check if the command is a key in the current room’s dict (i.e., a valid exit).- If it is,
room[command]gives the next room name; updatecurrent. - If not, print “you can’t go that way” and loop again.
This game uses every Phase 3 concept. Look at what’s in
those 30 lines: dicts, lookup, iteration, conditionals,
loops, input, output, the in operator, f-strings, string
methods. You wrote a small but complete game.
Checkpoint: You have a working text adventure with at least three rooms connected by exits, and you can navigate from a starting room to a winning room. This is the natural stop point if class is cut short.
Part B: Make it your adventure
The base game works. Now make it yours.
Base goal — your own world
Replace the rooms dictionary with your own. Build a world that makes sense to you — a house you’ve been in, a fantasy setting, a school, a spaceship, anywhere. Aim for at least six rooms with interesting connections.
Don’t make a straight line — branch the paths. Make some rooms dead-ends. Make the path to the win room have at least one “choice point” where the player has to pick a direction.
rooms = {
"front_yard": {
"description": "A small front yard. The house is north. A path leads east to the garden.",
"north": "house_door",
"east": "garden"
},
# ... your rooms here ...
}
Test it. Walk through the entire world to make sure every exit goes where you intended.
Stretch — items
Some rooms have items the player can pick up:
rooms = {
"kitchen": {
"description": "A bright kitchen. There's a cookie on the counter.",
"south": "entry",
"items": ["cookie"]
},
# ...
}
The "items" key holds a list of items in the room.
Add an inventory variable:
inventory = []
Add commands for take and inventory:
elif command.startswith("take "):
item = command[5:] # remove "take " (5 chars)
if "items" in room and item in room["items"]:
room["items"].remove(item)
inventory.append(item)
print(f"You picked up the {item}.")
else:
print("There's no such thing here.")
elif command == "inventory" or command == "i":
if len(inventory) == 0:
print("Your inventory is empty.")
else:
print(f"You have: {', '.join(inventory)}")
The command.startswith("take ") checks if the command begins
with “take “ — that’s how we recognize “take cookie” as a
take-something command. The command[5:] slices off “take “
and leaves just the item name.
', '.join(inventory) is a string method that joins a list
into one string with separators. ["cookie", "key"].join(", ")
becomes "cookie, key". (Not actually a method on the list
— it’s a method on the separator string. Weird Python
choice, but that’s the syntax.)
Update room descriptions to mention items, and add items
keys to the rooms that have them.
Stretch — a look command
Sometimes the player wants to re-read the current room’s description without moving:
elif command == "look" or command == "l":
# the description prints automatically next loop iteration
pass
Or for a richer version, list the available exits explicitly:
elif command == "look" or command == "l":
print(f"\n{room['description']}")
exits = []
for key in room:
if key not in ("description", "items"):
exits.append(key)
print(f"Exits: {', '.join(exits)}")
This iterates the room dict’s keys, skipping the description and items keys (which aren’t exits), and lists the rest.
Extension — locked doors
Some exits require a specific item to open:
"library": {
"description": "A library. The west door is locked.",
"west": "entry",
"west_requires": "key"
},
Then in the movement code:
elif command in room:
next_room = room[command]
# check for lock
requires = command + "_requires"
if requires in room:
needed = room[requires]
if needed not in inventory:
print(f"You need a {needed} to go that way.")
continue
current = next_room
Now the player has to find the key (in some other room) before they can use the locked door. Real puzzle-game logic.
Extension — load rooms from a file
Hard but rewarding. The room data is currently in your code.
Pull it into a separate file (rooms.txt or — fancier — a
rooms.json).
JSON format is what Python uses for dict-like data:
{
"entry": {
"description": "...",
"north": "kitchen"
}
}
Loading:
import json
with open("rooms.json", "r") as f:
rooms = json.load(f)
Now you can edit the world without touching the Python code. Same code-vs-data separation pattern from hangman.
Wrap-up
Before we leave, share with the room:
- For the kids who built their own world — what’s it set in? Walk us through finding the win room.
- For the kids who added items — what items did you include? Did anyone build a real puzzle (need item X to get item Y to reach room Z)?
- Did anyone get their parent or sibling to play it?
You built a complete game with multiple systems today. Not just a “play one round” game like hangman or the number-guesser — a world the player can explore. With items and locks (if you added them), this is a real puzzle game.
You also used almost every concept from Phase 3 — Python syntax, types, strings, lists, dicts, loops, conditionals, functions, file I/O, error handling — all in one program. That’s the proof that you have the toolkit.
Two more sessions of Phase 3 — milestone planning and demos — and then you move into Phase 4: Intermediate Python and the command line. You’re well on your way.
If you missed this session
Open Thonny and start a new file. Save as adventure.py.
Then:
- Build the rooms dictionary (Part A) — at least three rooms with exits.
- Build the game loop (the full Part A code).
- Run it. Navigate from entry to study (or wherever your win room is). Quit.
- Replace the rooms with your own world — at least six rooms.
About 50-60 minutes for the base. If you want items or locked doors (Part B), add another 30 minutes.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Multiple win conditions — find the treasure AND escape to the front door. Track multiple flags.
- NPCs (characters) — some rooms have a character who
says something, or who you have to talk to (with a
talkcommand). - Dynamic descriptions — the description changes based on whether you’ve taken something, picked up a clue, etc.
- Inventory limits — only carry 3 items; have to drop one to pick up another.
- A map command — print the world’s layout (advanced; requires thinking about how to render dict relationships visually).
- Save and load — save your current state to a file so you
can resume later. (Use
json.dump(...)andjson.load(...).)
What’s next
Next week starts your milestone project — your program, your design. You have two sessions to plan, build, and demo. Your milestone could be:
- A more elaborate text adventure
- A different kind of game (riddle game, trivia, puzzle)
- A useful tool (a quiz study app, a budget tracker, a daily journal)
- A creative program (a poem generator, a story builder)
- Anything else you can imagine that uses Python
Start thinking about what you want to build. Bring an idea to next week’s class.
Session 15: Milestone project work day 1
Session 15: Milestone project work day 1
Phase 3 — Python basics · Session 15 of 16
What we’re learning today
Today is your day. You’ll plan a Python program of your own choosing, then start building it. There’s no new programming concept this week — instead, you’ll use everything you’ve learned in Phase 3 to make something that’s yours, not Mr. Eric’s. Next week you’ll finish it and demo it to the class.
You’ll need to remember from last time
- Everything from Sessions 1-14. Print, input, types, strings, conditionals, loops, functions, return values, lists, dicts, file I/O, error handling.
- Your text adventure from last week — proof that you can build a substantial Python program.
- The idea you brought with you today. (If you didn’t, Part A has a list to start from.)
Part A: Plan your project
The most important step in any project isn’t typing — it’s deciding what to build. A clear plan saves hours of flailing later.
The plan
Take a piece of paper or open a blank text file. Answer these five questions about your project. Keep it short:
-
What’s the program? (One sentence. Examples: “A trivia game with Bible questions.” “A daily journal that saves to a file.” “A story generator that mixes random elements.” “A calculator that handles tip and tax.”)
-
What does the user do? (How do they interact? Type answers? Pick from a menu? Both?)
-
What functions or data structures will you need? (Brainstorm 3-5. Examples: a
dictof trivia questions, a functionask_question(q), a functionsave_journal(text), a list of items, etc.) -
What’s the simplest version that’s still cool? (If you only had today and next week to build this, what would you do FIRST? What would you add LATER if there’s time?)
-
What might go wrong, and how will you handle it? (Bad input? Missing file? Wrong type? Use try/except or
ifvalidation. Worth thinking about up front.)
If you don’t have an idea
Pick one and modify it slightly:
- A trivia game — questions stored in a list of dicts (or a file). User answers; track score. Multiple categories?
- A budget tracker — log income and expenses to a file; show totals and remaining; persist between runs.
- A daily journal — append today’s entry to a file with a date stamp; let the user read past entries.
- A study flashcard app — load cards from a file; quiz the user; track which ones they got wrong.
- A name generator — combine random first names and last names from lists; build a fantasy/normal/silly name for a character.
- A “mad libs” story builder — load a story template with blanks; ask the user for words; print the filled-in story.
- An extended text adventure — take last week’s adventure and add items, locked doors, NPCs, multiple endings.
- A simple bank account — deposit, withdraw, check balance; persist between runs.
- A favorite Bible verses collection — add, search, random-of-the-day; saved to a file.
Pick one. Spend two minutes deciding. Don’t agonize.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question that helps you tighten it up.
Start building — the simplest version first
Look at your answer to question 4. Build that first. Get the simplest version running before adding anything fancy.
If your plan is “trivia game with score and difficulty levels,” the simplest version might be:
- One hardcoded question.
- Ask it.
- Print “right” or “wrong.”
Then iterate: add a list of questions. Then a score. Then loading from a file. Then difficulty levels. Each step working before the next starts.
This is iterative development. Real programmers do this. The kid who writes 200 lines and then tries to debug all of it at once will have a much harder time than the kid who writes 20 lines, tests, adds 20 more, tests again.
When you get stuck
The five-step checklist from Getting unstuck — read the code, read the error, narrow down, rubber duck, ask buddy — plus today’s specific tools:
- Use the Thonny debugger (Session 5). Step through your code. Watch the variables.
- Use
print()to inspect. Addprint(variable)lines to see what’s happening. Remove them later. - Open your textbook chapters. Sessions 1-14 are your reference. If you forgot how lists work, Session 8 is right there.
- Ask your buddy. Explain what you’re trying to do.
- Ask Mr. Eric. Last resort, after the first four.
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today in your project. Could be a function. Could be a menu. Could be reading a file correctly. Whatever’s working — that’s a win.
Bring your project file (or just your machine) next week. We’ll finish, then demo to the class.
If you missed this session
Open Thonny and start a new file. Then:
-
Spend 10-15 minutes answering the five planning questions above on paper or in a text file.
-
Start building the simplest version of your project (look at your answer to question 4).
-
When you come next week, you’ll be ready to keep building and finish for the demo.
If you don’t have an idea, the seed list above is a starting point. Pick one and modify.
Stretch and extension ideas
If your base project is working and you want to add more:
- Save state to a file so the program remembers between runs (Session 11).
- Wrap risky input in
try/exceptso the program doesn’t crash on bad data (Session 13). - Refactor into functions so the main code is short and readable (Session 6).
- Add a menu so the user can pick what to do (Session 9 / 10).
- Use a list or dict of dicts to organize complex data (Sessions 8-10).
- Add ASCII art to make output visually interesting (the visual hangman pattern from Session 12).
Whatever you add, add it one piece at a time and test after each piece. Big changes that touch many things are how programs break.
What’s next
Next week you’ll have time to finish, polish, and demo your project to the rest of the class. Each person will get 3-5 minutes. Don’t worry — it’s a friendly demo, not a test. Bring a working program, bring your enthusiasm, and the rest takes care of itself.
Session 16: Milestone project work day 2 + demo day
Session 16: Milestone project work day 2 + demo day
Phase 3 — Python basics · Session 16 of 16
What we’re learning today
Today is the last session of Phase 3. You’ll spend the first half finishing and polishing your project — fixing bugs, adding the last features, making sure it runs. Then in the second half, you’ll demo your project to the class. By the end, you’ll have shipped your second milestone project (the first was Phase 1’s Scratch game) and you’ll be ready for Phase 4.
You’ll need to remember from last time
- Your project plan from last week.
- Whatever you got working — the foundation we’ll finish on today.
- Your buddy — they’ll help you test your program one last time before demos.
Part A: Final polish
You have about 30-40 minutes to finish your project. Be honest about what’s left.
What “finished” means
A finished milestone project is:
- It runs without crashing when you use it normally.
- It does what your plan said it would do — at least the simplest version (your answer to question 4 from last week).
- You can explain how it works. If someone asks “how does the score get tracked?” or “where does it save the journal?” you can walk them through it.
If your project doesn’t meet all three, focus on getting it there before adding anything new. A working basic project beats a broken ambitious one.
Add a final layer of polish
If your basic project works, spend the remaining time on polish — small things that make it nicer to use:
- Friendly error messages. Wrap risky input in
try/except(Session 13). Tell the user what went wrong, don’t crash. - Clear prompts. “Enter score: “ is clearer than “>”. A quick sentence of context at the start helps the user understand.
- Output formatting. Use f-strings with formatting
specifiers (
:.2ffor 2 decimal places, etc.). Add blank lines between sections (print()with no args). - Comments in your code explaining the trickier bits. Future-you will thank you.
- A title banner at startup so the program announces itself.
These small touches make a program feel finished instead of just barely working.
Buddy test
About 15 minutes in, swap with your buddy. They use your program for two minutes; you use theirs. Notice:
- Did it work without help?
- Did anything crash or do something unexpected?
- Could they tell what to do without instructions?
Tell each other one thing you noticed. This is the most useful feedback you’ll get.
Checkpoint: Your project runs the basic version of what you planned, and you’ve had at least one buddy try it. This is the natural stop point if class is cut short — but today, we keep going to the demo half.
Part B: Demo day
This is the fun part. Each of you will get 3-5 minutes to demo your program to the class.
How a demo works
When it’s your turn:
- Show your program. Run it. Use it for a minute or two. Let the class see what it does.
- Tell us about it. What’s it called? What’s it for? What does it do?
- Tell us one thing that was hard. Honest, not bragging. “Getting the file to save correctly was tricky.” “I couldn’t figure out why my loop was running twice.” Bug stories are the best stories.
- Take one question. Someone in the room asks something specific. Answer as best you can.
How to be a good audience
When it’s not your turn:
- Watch carefully. Pay attention to what other people built.
- Be ready to ask one question. Specific is better. “How did you make the trivia questions load from the file?” beats “That was cool, how’d you make it?”
- Be encouraging. Mr. Eric will say something specific about each project at the end; you can do the same as a buddy comment.
After everyone demos
Mr. Eric will say a few things about each of you and what he saw across the projects. Then we’re done — Phase 3 is over.
What you accomplished
- You shipped your second milestone project. Most adults haven’t shipped two creative projects ever. You’re at two.
- You handled real Python concepts: data structures, control flow, file I/O, error handling. That’s enough to write small programs that actually do things in the real world.
- You finished Phase 3. Sixteen sessions. Roughly 30 hours of class. The foundational programming skills are in place.
What’s next: Phase 4 — Intermediate Python, the CLI, and Git
Phase 4 is the next 8-10 sessions. What’s coming:
- Intermediate Python: working with multiple files, modules, slightly more advanced patterns. Your programs get bigger — more than one file each.
- The command line (CLI): until now, you’ve run Python
through Thonny’s green button. The command line is how
professionals run programs. You’ll learn
cd,ls, andpython myscript.py— the moves you’ll use forever. - Git: the most important tool for tracking your code. Lets you save versions of your work, undo mistakes, and (eventually) share with the world. We’ll start local, then go to GitHub in Phase 6.
After Phase 4, you’ll have everything you need to build real projects with multi-file structure, version control, and command-line workflow. The toolkit gets professional.
Bring your machine and your brain. See you in Phase 4.
If you missed this session
Two cases:
If you missed only the demo half: Show your project to Mr. Eric at the start of next week’s class. Same kind of feedback you’d have gotten in the demo.
If you missed the whole session and don’t have a finished project: No big deal. Finish your project at home using your Session 15 plan, and bring it next week. Or: just join us in Phase 4 — what you’ve already built is plenty to be proud of.
Stretch and extension ideas
If you have time after the demos and want to keep going:
- Polish your project more. Add more features, save state to a file, handle more edge cases.
- Build a second project of a different style. With everything you know now, the second one is faster than the first.
- Show your program to your family. They will be impressed. This is a fact.
- Save your project files. You’ll want them later. By the end of Phase 8, you’ll be amazed how far you came.
What’s next
Phase 4 — Intermediate Python, the command line, and Git. Real-world programmer tools. See you there.
Phase 4 — Intermediate Python, the CLI, and Git
Phase 3 gave you Python’s basic toolkit. Phase 4 gives you the professional toolkit — the way real programmers actually work. Three big additions: the command line (running programs without an editor’s green button), Git (version control for your code), and multi-file programs (because real projects don’t fit in one file).
What this phase is
Nine sessions covering the workflow tools that make programming feel like a job, not a hobby. Lots of new ideas about how to work rather than new Python syntax. By the end, you’ll be running scripts from the terminal, organizing code across multiple files, using Git to save versions of your work, and writing simple tests to verify your code does what you think.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | The command line | Open a terminal; cd, ls, python script.py |
| 2 | Multi-file programs | Splitting code; import yourfile; modules |
| 3 | Standard library modules | os, sys, datetime, pathlib |
| 4 | A light intro to classes | class, __init__, methods |
| 5 | Git — saving versions | init, add, commit, log, diff |
| 6 | Git in practice | recovering, undoing, light branching |
| 7 | Testing your code | assert statements; simple test functions |
| 8 | Milestone project work day 1 | plan + build (with Git from day one) |
| 9 | Milestone project work day 2 + demo | finish + showcase |
The Python concepts here are smaller than Phase 3’s, but the workflow changes are huge. You go from “code in Thonny, click green button” to “code in Thonny, save, run from terminal, commit to Git.” That’s how every professional programmer works.
What you’ll build
Smaller projects throughout, one bigger milestone at the end:
- Sessions 1-3: small experiments to learn the new tools.
- Session 4: your first program with a class — maybe a
simple
BankAccountwith deposit/withdraw, or aPetyou feed and play with. - Sessions 5-6: a small project tracked in Git from start to finish.
- Session 7: tests for code you’ve already written.
- Sessions 8-9: a milestone project of your own design using all the new tools — multi-file, Git-tracked, tested.
What you’ll need
Same machine as Phase 3 — Linux Mint XFCE has a terminal built in (Applications → Terminal Emulator, or right-click the desktop). Git is pre-installed. Thonny still works for editing. Python is still Python.
The only new thing on the machine: you’ll spend more time in the terminal window than ever before.
How sessions work
Same shape as before:
- Part A introduces a new tool with a guided exercise. Stands alone if class gets cut short.
- Part B is open practice or a project that uses Part A. Often tiered (base / stretch / extension).
- Wrap-up to share what you did.
A note about the terminal
The terminal feels weird at first. It’s a black (or white) window where you type commands and the computer responds with text. No buttons, no menus. Just text in, text out.
Almost every professional programmer uses the terminal every day. By the end of Phase 4, it’ll feel normal. Trust the process.
A note about Git
Git is a tool that saves versions of your code. Every time you make a change worth keeping, you commit it. Later, if you break something, you can go back to a previous version. If you have ten files and you want to know what changed yesterday, Git tells you.
Git is the most important tool in modern programming after the language itself. Phase 4 introduces it locally (on your own machine, just for you). Phase 6 introduces GitHub — the website where programmers share Git repositories with each other.
Where to start
Session 1: The command line is where Phase 4 begins. We open a terminal and learn how to run Python from it.
When you’re stuck, the Getting unstuck appendix has the checklist. The Glossary will grow as Phase 4 introduces new terms.
Welcome to professional tooling. Let’s go.
Session 1: The command line — your first cd, ls, python
Session 1: The command line — your first cd, ls, python
Phase 4 — Intermediate Python · Session 1 of 9
What we’re learning today
Until now, you’ve run Python by clicking the green button in Thonny. Today we learn another way: the command line. You open a terminal window, type a command, and the computer does what you said. By the end of class, you’ll be running Python scripts from the terminal — the way every professional programmer in the world does it.
You’ll need to remember from last time
- Thonny — the editor where you’ve been writing and running Python.
- The shell at the bottom of Thonny — where output appeared when your programs ran.
- A few of your own .py files from earlier sessions — we’ll run them today.
Part A: Open a terminal
Find the Terminal Emulator on your computer. On Linux Mint XFCE, it’s usually:
- Applications menu → System → Terminal Emulator, OR
- Right-click on the desktop → Open Terminal Here
A black (or sometimes white) window opens. There’s a blinking
cursor and some text — probably your username and a $ or
% symbol. That’s the prompt. It’s the terminal saying
“type a command.”
What this is
The terminal lets you talk to your computer without using a mouse. You type a command, press Enter, the computer does something, and the result shows up in the same window.
The terminal is also called:
- The command line (because you type one line at a time)
- The shell (because it’s a “shell” around the operating system)
- The console
- bash (the specific shell program most Linuxes use)
These all mean roughly the same thing in our class.
Your first command — pwd
Type pwd and press Enter. (Stands for “print working
directory.”)
$ pwd
/home/your_username
It tells you where you are in the file system. By default, you start in your “home” directory.
The $ is the prompt; you don’t type it. Just the pwd.
Throughout this chapter, lines starting with $ are
commands you type; the rest is what the terminal prints back.
ls — what’s here?
Type ls (LS — for “list”):
$ ls
Desktop Documents Downloads Music Pictures Public Videos
You see the things in your home directory — folders and files.
Try ls -la:
$ ls -la
total 96
drwxr-xr-x 18 you you 4096 Apr 25 09:00 .
drwxr-xr-x 3 root root 4096 Apr 1 12:00 ..
drwxr-xr-x 2 you you 4096 Apr 25 09:00 Desktop
...
The -la is a flag — extra options. -l means “long
format” (more details). -a means “include hidden files”
(files starting with .).
Most commands have flags. You don’t need to memorize them; you’ll learn the ones you use.
cd — go somewhere else
cd means “change directory.” Try:
$ cd Desktop
$ pwd
/home/your_username/Desktop
You moved to the Desktop folder. Now ls shows what’s on
the desktop.
To go back up to your home directory:
$ cd ..
$ pwd
/home/your_username
The .. means “the parent folder.” It’s a special name that
always means “one level up.”
To jump straight back home from anywhere:
$ cd
$ pwd
/home/your_username
cd with no argument always takes you home.
Try this on your own
Spend a few minutes navigating around. Try:
cd Documentsthenlscd Picturesthenlscd ..to go backcd ../Musicto go up and over
Get comfortable moving around. The terminal is just a way of walking through your folders.
Checkpoint: You’ve used pwd to see where you are,
ls to list files, and cd to navigate to a different
folder. This is the natural stop point if class is cut
short.
Part B: Running Python from the terminal
Time for the actual point of all this — running Python without the green button.
Find one of your old programs
You have lots of .py files saved from previous sessions —
hangman, the contact book, the text adventure. Pick one.
In the terminal, navigate to the folder where that file is.
For example, if your hangman is in Documents/python:
$ cd Documents/python
$ ls
hangman.py notes.txt words.txt
You see your file.
Run it
Type:
$ python hangman.py
(On some systems it’s python3 instead of python. Try
python first; if “command not found”, use python3.)
The program runs — same as if you’d hit the green button in Thonny. Same input, same output, same everything. But now you’re running it from the terminal.
When the program ends, you’re back at the $ prompt, ready
for another command.
Why this matters
Three reasons:
- No editor required. You can run a Python script anywhere, anytime, without opening Thonny.
- Other tools work the same way. Most professional tools (Git, Node.js, web servers, you name it) are command-line first. Learning the terminal opens up all of them.
- It’s faster. Once you’re used to it, running a
script with
python myscript.pyis faster than clicking through menus.
Try a few
Pick three different .py files you’ve written. Navigate to
each and run it from the terminal:
$ cd Documents/python
$ python hangman.py
[play hangman]
$ python adventure.py
[explore the adventure]
$ python contacts.py
[manage contacts]
This is your new daily workflow.
Stretch — useful CLI commands
Some other commands you’ll use over and over:
| Command | What it does |
|---|---|
mkdir new_folder | make a new folder |
rm filename | delete a file (be careful — no undo) |
cp source destination | copy a file |
mv source destination | move (or rename) a file |
cat filename | show the contents of a text file |
clear | clear the terminal screen |
Try cat words.txt in your hangman folder — your word list
prints out.
Try mkdir test_folder then cd test_folder then ls.
Be careful with
rm. It’s permanent — there’s no trash can. If you delete a file by accident, it’s gone. Always look at what you’re deleting first.
Stretch — python -i
python -i myscript.py runs your script and then drops you
into an interactive shell with all your variables still
defined. Useful for poking at your program after it runs.
Extension — make your script feel like a real command
Add this line at the very top of one of your scripts:
#!/usr/bin/env python3
(Yes, that exact text — including the #!.)
Then in the terminal:
$ chmod +x myscript.py
$ ./myscript.py
Now you can run it like a normal command. The first line
tells your system “use Python to run this.” The chmod +x
makes the file executable. The ./ says “run this from
the current folder.”
This is how real programmers package their tools. Phase 4’s later sessions will use this pattern.
Wrap-up
Before we leave, share with the room:
- For the kids who ran one of your earlier programs from the terminal — did it feel different from clicking the green button?
- Did anyone get lost in the file system? How did you find your way back?
- For the stretch commands — anyone use
mkdirorcat? What for?
You learned today the most-used tool in all of professional programming. Every developer uses the terminal every day. You don’t have to like it; you do have to be comfortable with it.
The terminal is also how you’ll use Git in a few sessions
— Git is a command-line tool. Today’s cd and ls and
python are the foundation; Git’s commands will feel natural
once these do.
If you missed this session
Open a terminal on your computer (Applications → Terminal, or right-click desktop → Open Terminal). Then:
- Type
pwdand press Enter. See where you are. - Type
ls. See what’s in this folder. - Type
cd Documents(or wherever your.pyfiles are). Thenlsagain. - Type
python yourfile.pyto run a Python file from the terminal.
About 20-30 minutes of practice. The more you use it, the faster it gets.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Tab completion — start typing a folder or file name, press Tab. The terminal completes it for you. Saves a lot of typing.
- Up arrow — recalls previous commands. Useful for re-running something.
history— shows your command history.- Pipes —
cat words.txt | wc -lcounts the lines in the file. The|pipes the output of one command into another. Powerful and confusing; mention if curious. - Output redirection —
python myscript.py > output.txtsaves the output to a file instead of printing it.
What’s next
Next week we’ll learn how to split a Python program into
multiple files — because real programs don’t fit in one
file. We’ll learn how to import your own files, just like
you’ve been importing random and turtle.
Session 2: Multi-file programs and your own modules
Session 2: Multi-file programs and your own modules
Phase 4 — Intermediate Python · Session 2 of 9
What we’re learning today
Until now, every program you’ve written has lived in one
file. Real programs don’t work that way — they’re spread
across many files, each one focused on a specific job.
Today you’ll learn how to split your code across files and
how to import your own files the same way you’ve been
importing random or turtle. By the end of class, you’ll
have a multi-file program with its parts cleanly separated.
You’ll need to remember from last time
- The terminal —
cd,ls,python script.py. import— used in Phase 2 (import turtle) and Phase 3 (import random). Today we use the same syntax with our own files.- Functions and classes (we’ll add classes in Session 4 — for now, functions are enough).
Part A: Splitting a program in two
Open Thonny and start a new folder for today. Create a folder
on your desktop called today_session. Inside it, create two
empty Python files:
tools.pymain.py
(Use Thonny: File → New, save as tools.py. Repeat for
main.py. Or create the files in a file manager. Either
works.)
The motivation
Suppose you wanted to build a small program that does math: add, subtract, multiply, divide. You could put it all in one file:
# all_in_one.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
x = int(input("Enter first number: "))
y = int(input("Enter second number: "))
op = input("Operation (+, -, *, /): ")
if op == "+":
print(add(x, y))
elif op == "-":
print(subtract(x, y))
elif op == "*":
print(multiply(x, y))
elif op == "/":
print(divide(x, y))
This works. But the math functions could be useful in other programs too — a budget tracker, a grade calculator, anywhere you need math. Why not put them in their own file?
Step 1: Put the math functions in tools.py
Open tools.py in Thonny. Type:
# tools.py — math helper functions
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def multiply(a, b):
return a * b
def divide(a, b):
return a / b
Save. That’s it. No input, no print, no main code. Just
function definitions. This file is now a module — a file
of reusable code.
Step 2: Use the module from main.py
Open main.py. Type:
# main.py — the calculator program
import tools
x = int(input("Enter first number: "))
y = int(input("Enter second number: "))
op = input("Operation (+, -, *, /): ")
if op == "+":
print(tools.add(x, y))
elif op == "-":
print(tools.subtract(x, y))
elif op == "*":
print(tools.multiply(x, y))
elif op == "/":
print(tools.divide(x, y))
Save.
Now run it from the terminal. Open a terminal, navigate to
your folder, and run main.py:
$ cd Desktop/today_session
$ python main.py
Enter first number: 5
Enter second number: 3
Operation (+, -, *, /): +
8
It works! And the math functions live in their own file.
What’s happening
import tools tells Python to load the tools.py file. After
that, you access its functions with tools.add(...) — the
file name (without .py), then a dot, then the function
name.
This is the same pattern as import random then
random.randint(1, 100). Same syntax. Just our own file
this time instead of a built-in module.
from ... import ...
There’s a shorter way to import:
from tools import add, subtract, multiply, divide
Now you can call them by their plain names — no tools.
prefix needed:
print(add(x, y)) # not tools.add
Both styles work. The from ... import ... style is cleaner
when you only use a few specific things from a module. The
plain import tools is cleaner when you use lots of things
from it (or when you want it to be obvious in your code where
something came from).
Pick one style and use it consistently in any one file. Mixing them is confusing.
Why split files matters
A few reasons:
- Reuse. The math functions in
tools.pycan now be imported into ANY program. Not just this calculator — anything that needs math. - Organization. A single file with 500 lines is hard to navigate. A folder with five files of 100 lines each is much easier.
- Working with others. When you have a project with many people, each person can work on different files without stepping on each other.
- Testing. Tests (Session 7) are usually in a separate file from the code they test.
This is one of those moves that doesn’t change what you can build — but it changes how manageable big programs are. Phase 4’s projects from here on will be multi-file.
Checkpoint: You have two files (tools.py and
main.py) where main.py imports and uses functions from
tools.py, and you’ve run main.py from the terminal.
This is the natural stop point if class is cut short.
Part B: A multi-file project
Now build something more substantial.
What you’re building
A small program with three files working together — a journal that tracks daily entries with a quote-of-the-day feature.
Plan it out
Three files:
quotes.py— has a list of quotes and a functionrandom_quote()that returns onejournal.py— has functions to read and write journal entriesmain.py— the user-facing program that uses both
Each file is small and focused. The main file is the only one with input/output and the program flow.
Build quotes.py
# quotes.py
import random
QUOTES = [
"The journey of a thousand miles begins with a single step.",
"Be the change you wish to see.",
"Patience is bitter, but its fruit is sweet.",
"He who has a why can endure any how.",
"The only way out is through.",
]
def random_quote():
return random.choice(QUOTES)
Save.
(Capitalized variable names like QUOTES are a Python
convention for constants — values that aren’t expected
to change while the program runs. It’s a hint to readers,
not a Python rule.)
Build journal.py
# journal.py
JOURNAL_FILE = "journal.txt"
def add_entry(text):
with open(JOURNAL_FILE, "a") as f:
f.write(text + "\n")
def read_entries():
try:
with open(JOURNAL_FILE, "r") as f:
entries = []
for line in f:
cleaned = line.strip()
if cleaned:
entries.append(cleaned)
return entries
except FileNotFoundError:
return []
Save.
A walk-through of the new bit inside the with:
- Open the file and loop over each line in turn.
cleaned = line.strip()—.strip()removes the trailing newline character and any whitespace. (Same.strip()you put on everyinput()back in Phase 3 — same reason here.)if cleaned:— skip blank lines.entries.append(cleaned)— add the cleaned line to our growing list.- After the loop, return the list.
Reading lines from a file is the most common place you’ll
need .strip(). Every line carries a trailing newline you
almost never want.
(Real Python programmers usually compact this loop into a single line called a list comprehension — they’re a Phase 4 stretch goal. For now the explicit loop is the right shape to learn.)
Build main.py
# main.py
import quotes
import journal
def main():
print("Welcome to your journal!")
print(f"\nQuote of the day: {quotes.random_quote()}\n")
print("Past entries:")
entries = journal.read_entries()
if len(entries) == 0:
print(" (none yet)")
else:
for i, entry in enumerate(entries):
print(f" {i + 1}. {entry}")
print("\nAdd a new entry (or press Enter to skip):")
new_entry = input("> ").strip()
if new_entry:
journal.add_entry(new_entry)
print("Saved!")
main()
Save.
The def main() and main() at the bottom is a common
Python pattern. Wrap the program’s main code in a function
called main, then call it at the bottom. It makes things
easier to read and is a habit you’ll thank yourself for later.
Run it
$ cd Desktop/today_session
$ python main.py
Welcome to your journal!
Quote of the day: The only way out is through.
Past entries:
(none yet)
Add a new entry (or press Enter to skip):
> Today I learned about multi-file programs.
Saved!
Run it again. Your entry from before is now in the “past entries” section. The quote changes each run. It works.
That’s the base goal — three files, each focused, all working together.
Stretch — add a “search” feature
Add a function to journal.py:
def search_entries(keyword):
entries = read_entries()
results = []
for entry in entries:
if keyword.lower() in entry.lower():
results.append(entry)
return results
In main.py, add a menu option to search:
def main():
print("Welcome to your journal!")
while True:
print("\nWhat do you want to do?")
print(" 1. Show quote of the day")
print(" 2. Show past entries")
print(" 3. Add new entry")
print(" 4. Search entries")
print(" 5. Quit")
choice = input("> ")
if choice == "1":
print(f"\n{quotes.random_quote()}")
elif choice == "2":
entries = journal.read_entries()
for i, entry in enumerate(entries):
print(f" {i + 1}. {entry}")
elif choice == "3":
text = input("Entry: ")
journal.add_entry(text)
print("Saved!")
elif choice == "4":
keyword = input("Search for: ")
results = journal.search_entries(keyword)
if len(results) == 0:
print("No matches.")
else:
for entry in results:
print(f" - {entry}")
elif choice == "5":
break
main()
Now you have a real journal app — multi-file, menu-driven, search-capable.
Extension — load quotes from a file
Pull the quotes out of quotes.py and into quotes.txt:
The journey of a thousand miles begins with a single step.
Be the change you wish to see.
Patience is bitter, but its fruit is sweet.
Then quotes.py reads from the file:
# quotes.py
import random
QUOTES_FILE = "quotes.txt"
def _load_quotes():
quotes = []
with open(QUOTES_FILE, "r") as f:
for line in f:
cleaned = line.strip()
if cleaned:
quotes.append(cleaned)
return quotes
def random_quote():
return random.choice(_load_quotes())
The leading underscore on _load_quotes is a Python
convention meaning “this function is private — meant only
for use inside this module.” Readers know not to use it from
elsewhere.
Now you can edit quotes.txt to add or change quotes,
without touching the Python code. Code and data, separated
again.
Wrap-up
Before we leave, share with the room:
- For the kids who built the journal — does multi-file feel cleaner than one big file?
- For the kids who added the search feature — was it easy to
add to
journal.pywithout touchingmain.py? - Did anyone get an
ImportErrororModuleNotFoundError? What fixed it?
You learned today the most important organizational idea in real programming. Real codebases are dozens or hundreds of files, each focused on one thing, all importing from each other. Today’s three-file journal is the prototype for every project from here on.
You also met a few Python conventions: UPPERCASE_NAMES for
constants, _underscore_prefix for private helpers, and the
def main(): ...; main() pattern for the program’s main code.
These conventions aren’t rules — Python doesn’t enforce them
— but every Python programmer follows them, so your code
looks like real Python.
If you missed this session
Open Thonny and create a folder. Inside it:
-
Create
tools.pywith a few simple functions likeaddandsubtract. -
Create
main.pythat doesimport toolsand usestools.add(2, 3). -
Open a terminal,
cdinto your folder, runpython main.py. -
Build the three-file journal from Part B.
About 40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- List comprehensions. Python lets you collapse a
for/if/append loop into one line:
Read it as “the cleaned line for each line in f, but only if the cleaned line isn’t empty.” It does exactly what your explicit loop does. Try replacing your loop with it inentries = [line.strip() for line in f if line.strip()]journal.pyand confirm the program still works. List comprehensions are everywhere in real Python; the explicit loop is the right shape to learn first, but you should recognize the compact form when you see it. - Subfolders for modules. Put related modules in a
subfolder. To make it work as a package, add an empty
__init__.pyfile in the subfolder. Thenimport myfolder.mymodule. - Renaming on import.
import really_long_module_name as mod— now you call itmod.something()for short. from module import *— imports everything. Don’t do this; it pollutes your namespace and confuses readers. Mention only as a “what not to do.”- Circular imports. When file A imports B and B imports A. Confusing and error-prone. Mention only if a kid hits it.
What’s next
Next week we explore Python’s standard library — the huge
collection of modules that come with Python. You’ve already
used a few (random, turtle). There are dozens more for
working with dates, files, paths, web pages, and more. We’ll
tour the most useful ones.
Session 3: Standard library modules
Session 3: Standard library modules
Phase 4 — Intermediate Python · Session 3 of 9
What we’re learning today
Python comes with a huge collection of pre-written code called
the standard library — modules for working with dates,
files, paths, web pages, and dozens of other things. You’ve
already met two (random and turtle). Today we tour several
more useful ones and use them to build a real CLI tool: a
daily journal you run from the terminal.
You’ll need to remember from last time
import yourfile— same syntax for stdlib modules.- The terminal — running scripts with
python script.py. - File I/O from Phase 3 —
open(...), append mode"a". - f-strings for formatting output.
Part A: A tour of useful modules
Open Thonny and start a new file. Save it as tour.py.
datetime — dates and times
The datetime module handles dates, times, and date math.
from datetime import datetime
now = datetime.now()
print(now)
today = now.strftime("%Y-%m-%d")
print(today)
time = now.strftime("%H:%M")
print(time)
Run.
2026-04-25 14:32:18.123456
2026-04-25
14:32
datetime.now() returns the current date and time. .strftime(...)
formats it as a string. The %Y-%m-%d and %H:%M are
format codes — %Y means 4-digit year, %m is 2-digit
month, etc. There are dozens of format codes; look them up
when you need them.
You can also do date math:
from datetime import datetime, timedelta
now = datetime.now()
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(days=7)
print(f"Now: {now.strftime('%Y-%m-%d')}")
print(f"Tomorrow: {tomorrow.strftime('%Y-%m-%d')}")
print(f"Last week: {last_week.strftime('%Y-%m-%d')}")
timedelta(days=1) represents a day. Add or subtract from a
datetime to get a different date. Useful for “calculate the
date 30 days from now” type problems.
pathlib — modern file paths
pathlib is the modern way to handle file paths in Python.
Cleaner than the older os.path style.
from pathlib import Path
home = Path.home()
print(f"Home: {home}")
current = Path.cwd()
print(f"Current: {current}")
# build a path
notes_file = home / "notes.txt"
print(f"Path: {notes_file}")
# check if it exists
print(f"Exists? {notes_file.exists()}")
Walk through:
Path.home()— your home directory.Path.cwd()— the current working directory (where you’re running from).home / "notes.txt"— uses/to combine paths. Way cleaner than string concatenation..exists()— does this file or folder actually exist?
Path also has methods like .is_file(), .is_dir(),
.mkdir(), and many others.
sys — command-line arguments
Remember how the terminal lets you pass extra arguments to
commands? Like cp source destination or mkdir new_folder?
Your Python scripts can take arguments too.
import sys
print(f"Number of arguments: {len(sys.argv)}")
print(f"All arguments: {sys.argv}")
if len(sys.argv) > 1:
print(f"First argument: {sys.argv[1]}")
sys.argv is a list. The first item (sys.argv[0]) is
always the script name. The rest are whatever the user
typed after.
Run from the terminal:
$ python tour.py hello world
Number of arguments: 3
All arguments: ['tour.py', 'hello', 'world']
First argument: hello
This is how real CLI tools work — git commit "message",
python script.py argument, etc. Each space-separated word
becomes an item in argv.
About different machines: the class machines run Linux, where
python tour.py hello worldworks exactly as shown. If you try this from a Windows home laptop andpythonisn’t found, trypy tour.py hello worldinstead. Same behavior —sys.argvworks on every operating system once you can launch the script.
os — operating system stuff
os has lots of functions for interacting with the operating
system:
import os
# environment variables
home = os.environ.get("HOME", "unknown")
print(f"Home from env: {home}")
# list files in a directory
files = os.listdir(".")
print(f"Files here: {files}")
We mostly use pathlib instead of os.path these days, but
os still has useful environment-variable and process stuff.
Checkpoint: You’ve used datetime, pathlib, and sys
in at least one program each. This is the natural stop
point if class is cut short.
Part B: A daily journal CLI
Time to build something real that uses these modules.
What you’re building
A command-line journal. Each day gets its own file. You add entries by running:
$ python journal.py "Today I learned about modules"
Saved to /home/sam/journals/2026-04-25.txt
The script:
- Takes the entry text from
sys.argv. - Uses
datetimeto figure out today’s date. - Uses
pathlibto build the filename. - Appends the entry (with a timestamp) to today’s file.
Real CLI tool — runs from the terminal, takes an argument, does its job, exits.
Build it
# journal.py
import sys
from datetime import datetime
from pathlib import Path
JOURNAL_DIR = Path.home() / "journals"
JOURNAL_DIR.mkdir(exist_ok=True)
if len(sys.argv) < 2:
print("Usage: python journal.py <entry text>")
sys.exit(1)
# get the entry text (everything after the script name)
entry_text = " ".join(sys.argv[1:])
# build today's filename
today = datetime.now().strftime("%Y-%m-%d")
filename = JOURNAL_DIR / f"{today}.txt"
# append the entry with a timestamp
timestamp = datetime.now().strftime("%H:%M")
with open(filename, "a") as f:
f.write(f"[{timestamp}] {entry_text}\n")
print(f"Saved to {filename}")
Save. Then in the terminal:
$ cd Desktop/today
$ python journal.py "First entry of the day"
Saved to /home/sam/journals/2026-04-25.txt
Walk through what’s happening:
JOURNAL_DIR = Path.home() / "journals"— build a path to ajournalsfolder in the home directory..mkdir(exist_ok=True)— make the folder if it doesn’t exist. Theexist_ok=Truemeans “no error if it already exists.”len(sys.argv) < 2— check that the user provided at least one argument. If not, print usage and exit.sys.exit(1)— exit the program with status code 1 (meaning “error”). Any non-zero status code is conventional for “something went wrong.”" ".join(sys.argv[1:])— slice off the script name (the[1:]), then join the rest with spaces. So if the user typedpython journal.py hello world, you get"hello world"as a single string.- The rest is familiar — file path with
pathlib, append with"a"mode.
Try it a few times. Each entry gets appended to today’s file. Tomorrow’s entries will go to a different file automatically.
That’s the base goal.
Stretch — a “view” subcommand
Allow the user to view today’s entries. Detect the special
case of view as the argument:
# journal.py
import sys
from datetime import datetime
from pathlib import Path
JOURNAL_DIR = Path.home() / "journals"
JOURNAL_DIR.mkdir(exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
today_file = JOURNAL_DIR / f"{today}.txt"
if len(sys.argv) < 2:
print("Usage: python journal.py <entry text>")
print(" python journal.py view")
sys.exit(1)
if sys.argv[1] == "view":
if today_file.exists():
print(f"\n=== {today} ===")
with open(today_file, "r") as f:
print(f.read())
else:
print(f"No entries yet for {today}.")
else:
# add a new entry
entry_text = " ".join(sys.argv[1:])
timestamp = datetime.now().strftime("%H:%M")
with open(today_file, "a") as f:
f.write(f"[{timestamp}] {entry_text}\n")
print(f"Saved to {today_file}")
Now:
$ python journal.py view
=== 2026-04-25 ===
[14:32] First entry of the day
[14:45] Made the journal a real CLI tool
$ python journal.py "Adding a third entry"
Saved to /home/sam/journals/2026-04-25.txt
Two ways to use the same script — subcommands. Real CLI
tools (Git, Docker, etc.) work this way: git commit, git push, git status — different verbs, same git command.
Extension — view a specific day
if sys.argv[1] == "view":
if len(sys.argv) >= 3:
# view a specific date
date_arg = sys.argv[2]
target_file = JOURNAL_DIR / f"{date_arg}.txt"
else:
# view today
target_file = today_file
if target_file.exists():
with open(target_file, "r") as f:
print(f.read())
else:
print(f"No entries for that date.")
Now python journal.py view 2026-04-20 shows entries from
that date.
Extension — list all journals
if sys.argv[1] == "list":
files = sorted(JOURNAL_DIR.glob("*.txt"))
for f in files:
print(f.stem) # filename without extension
.glob("*.txt") returns all .txt files in the directory.
.stem is the filename without the extension (so
2026-04-25.txt → 2026-04-25).
Add this to your subcommand check. Now you have three commands: add (default), view, list. Real CLI app.
Wrap-up
Before we leave, share with the room:
- For the kids who built the base journal — does it feel weird to run it from the terminal with arguments?
- For the stretch — was the subcommand pattern clean?
- For the extension — anyone build a
deletecommand or acountcommand?
You learned today how to use Python’s standard library to
build real, useful CLI tools. The patterns you saw — sys.argv
for arguments, datetime for now-aware programs, pathlib
for file paths — are exactly how professional Python
scripts are built.
You also learned subcommands — the way real CLI tools
expose multiple actions through one entry point. git, npm,
docker, kubectl — they all work this way. Today’s journal
is a tiny version of the same shape.
If you missed this session
Open Thonny. Then:
- Try the
datetimeexamples from Part A — print today’s date and the time. - Try the
pathlibexamples — print your home directory. - Try
sys.argv— pass a few arguments when you run the script and print them. - Build the journal CLI from Part B base.
Run from the terminal each time. About 30-40 minutes.
If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
json— read and write JSON files. Useful for saving structured data.json.dump(data, f)to save;json.load(f)to load.csv— read and write CSV files (spreadsheet format).import csv.shutil— file operations like copy, move, delete whole directories.subprocess— run other programs from your Python script.subprocess.run(["ls", "-la"]).urllib.request— fetch content from URLs (basic web requests).
Browse the standard library docs when you’re curious — there’s a whole world to explore.
What’s next
Next week we add classes to your toolkit — a way to bundle
data and the functions that work on that data into a single
unit. Classes are how Path and datetime are built — and
how almost all “object-oriented” Python is structured.
Session 4: A light intro to classes
Session 4: A light intro to classes
Phase 4 — Intermediate Python · Session 4 of 9
What we’re learning today
Until now, your programs have been organized as functions operating on separate variables. Today you’ll learn how to bundle data and functions together into a single thing called a class. Classes let you say “this is a Pet, and here’s how to feed it, play with it, check on it” — all in one organized package. By the end of class, you’ll have built a Pet class with several methods, plus tested it with multiple pets.
You’ll need to remember from last time
- Functions with parameters and return values.
- The
defkeyword for defining functions. - Indentation for grouping code.
- Standard library classes like
Pathanddatetime— yes, those are classes. Today we learn how to make our own.
Part A: Your first class
Open Thonny and start a new file. Save it as pet.py.
The motivation
Suppose you wanted to track a pet — its name, whether it’s hungry, whether it’s happy. With what you know, you’d use separate variables:
pet_name = "Rex"
pet_hungry = True
pet_happy = True
def feed(name, hungry):
if hungry:
return False # not hungry anymore
return hungry
pet_hungry = feed(pet_name, pet_hungry)
This works but is awkward. The data (name, hungry, happy) and the functions that work on it (feed, play) are separate — you have to keep them organized in your head.
It gets worse with multiple pets:
pet1_name = "Rex"
pet1_hungry = True
pet2_name = "Whiskers"
pet2_hungry = True
# ... ugh
There’s a better way: classes.
Define a class
class Pet:
def __init__(self, name):
self.name = name
self.hungry = True
self.happy = True
def feed(self):
if self.hungry:
self.hungry = False
print(f"{self.name} eats happily.")
else:
print(f"{self.name} is not hungry right now.")
def play(self):
if self.hungry:
print(f"{self.name} is too hungry to play.")
else:
self.hungry = True # playing makes them hungry
print(f"{self.name} loves playing!")
def status(self):
h = "hungry" if self.hungry else "full"
m = "happy" if self.happy else "sad"
print(f"{self.name} is {h} and {m}.")
A lot of new pieces. Let’s break it down.
class Pet:
class starts a class definition. Pet is the class name.
The convention in Python is CapitalCase for class names
(unlike functions and variables which are lowercase). Just
like Path, datetime, etc. — those are classes too.
def __init__(self, name):
__init__ is a special method that runs when a new pet is
created. It’s called the constructor. The double
underscores around the name (called “dunder”) are a Python
convention for “this is a special method the language calls
for you.”
self is the first parameter, always. It refers to the
specific pet being created. Don’t worry about understanding
this fully yet — just know that self is always the first
parameter, and you don’t pass it when you call methods.
name is a normal parameter — the pet’s name when you create
it.
Inside __init__:
self.name = name— set this pet’s name to the value passed in.self.hungry = True— every new pet starts hungry.self.happy = True— every new pet starts happy.
The self.something = value syntax stores values on the
pet itself — they live with that specific pet, not with
the class as a whole.
def feed(self):
A regular method (function inside a class). self is again
the first parameter — it refers to the specific pet being
fed. We use self.hungry to check the pet’s hungry status,
and self.hungry = False to change it.
self.name reads the pet’s name. self.hungry reads the
pet’s hungry status. Methods on a class always take self
as the first parameter so they know which specific
instance to work with.
Use the class
Add this at the bottom of pet.py:
my_pet = Pet("Rex")
my_pet.status()
my_pet.feed()
my_pet.status()
my_pet.play()
my_pet.status()
my_pet.feed()
my_pet.status()
Save and run.
Rex is hungry and happy.
Rex eats happily.
Rex is full and happy.
Rex loves playing!
Rex is hungry and happy.
Rex eats happily.
Rex is full and happy.
Walk through:
Pet("Rex")— instantiates the class. Creates a new pet named Rex. Returns the new pet object, which we save inmy_pet.my_pet.status()— call thestatusmethod onmy_pet. Notice we don’t passself— Python passes it automatically. Theselfinside the method refers tomy_pet.- The pet’s state changes over time as we feed and play with it.
Multiple pets
The power of classes: you can have many independent pets:
rex = Pet("Rex")
whiskers = Pet("Whiskers")
rex.feed()
whiskers.status()
rex.status()
Two separate pets, each with their own name, their own
hungry status, their own happy status. Calling rex.feed()
doesn’t affect whiskers at all.
Each pet is an instance of the Pet class. The class is the blueprint; the instances are the actual pets you create from it.
Checkpoint: You’ve defined a class with __init__ and
at least one other method, created at least one instance of
it, and called methods on that instance. This is the
natural stop point if class is cut short.
Part B: Use it for real
Time to do something more substantial with classes.
Base goal — multiple pets
Build a small program where the user takes care of three different pets:
class Pet:
# ... (the class from Part A)
# create three pets
pets = [Pet("Rex"), Pet("Whiskers"), Pet("Spot")]
# show all
print("Your pets:")
for pet in pets:
pet.status()
# feed each one
print("\nFeeding everyone...")
for pet in pets:
pet.feed()
# show again
print("\nAfter feeding:")
for pet in pets:
pet.status()
Save. Run. The program walks through three pets, doing the same thing to each.
The interesting move: you have a list of objects, and you loop through it calling methods on each. Same pattern as looping through a list of strings or numbers — except the items are richer (they have data AND methods).
Stretch — interactive pet care
Add a menu-driven version:
class Pet:
# ... (same as before)
pets = [Pet("Rex"), Pet("Whiskers"), Pet("Spot")]
while True:
print("\nYour pets:")
for i, pet in enumerate(pets):
print(f" {i + 1}. {pet.name}")
print("\n1. Status all\n2. Feed one\n3. Play with one\n4. Quit")
choice = input("> ")
if choice == "1":
for pet in pets:
pet.status()
elif choice == "2":
i = int(input("Which pet (number)? ")) - 1
if 0 <= i < len(pets):
pets[i].feed()
elif choice == "3":
i = int(input("Which pet (number)? ")) - 1
if 0 <= i < len(pets):
pets[i].play()
elif choice == "4":
break
Now the user picks which pet to interact with from a menu. Real game-like feel, with a list of class instances under the hood.
Extension — a second class
Add a Treat class that pets can be fed:
class Treat:
def __init__(self, name, food_value):
self.name = name
self.food_value = food_value
def describe(self):
return f"a {self.name} (worth {self.food_value} hunger points)"
class Pet:
def __init__(self, name):
self.name = name
self.hunger = 5 # 0 = full, 10 = starving
self.happy = True
def eat(self, treat):
print(f"{self.name} eats {treat.describe()}.")
self.hunger -= treat.food_value
if self.hunger < 0:
self.hunger = 0
def status(self):
print(f"{self.name}: hunger {self.hunger}/10, "
f"{'happy' if self.happy else 'sad'}.")
# usage
rex = Pet("Rex")
cookie = Treat("cookie", 3)
steak = Treat("steak", 8)
rex.status()
rex.eat(cookie)
rex.status()
rex.eat(steak)
rex.status()
Two classes interacting:
Treathas a name and a food value (how much hunger it removes).Pethas a numeric hunger (instead of just True/False).Pet.eat()takes aTreatand reduces hunger by the treat’s food value.
Two classes, one program, working together. That’s how real OOP code is structured — many classes, each representing a thing in the program’s world, calling each other’s methods.
(This is a hint of where Phase 5’s customtkinter goes — Buttons, Frames, Labels, all classes that interact.)
Wrap-up
Before we leave, share with the room:
- For the kids who built the Pet class — what behavior did you add beyond feed/play? Anything custom?
- For the menu version — does having a list of pet objects feel cleaner than tracking each pet’s variables separately?
- For the two-class extension — was it natural for two classes to interact?
You learned today the foundational idea of object-oriented
programming — bundling data and functions into named
units. Real Python (and most other modern languages) is
mostly classes. Path is a class. datetime is a class.
The customtkinter widgets you’ll meet in Phase 5 are
classes. The Pygame sprites in Phase 6 are classes.
You’re not done with classes — Phase 5 and beyond build on them constantly. Today is the foundation: define a class, make instances of it, call methods on them. That’s enough to read and understand most class-based code you’ll see.
If you missed this session
Open Thonny. Then:
-
Type out the basic Pet class from Part A. Create one pet with
Pet("Rex"). Call.status(),.feed(),.play(). Watch the state change. -
Try creating two pets. Feed one. Verify the other isn’t affected.
-
Build the multi-pet base from Part B (a list of pets, a loop calling methods on each).
About 35-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Class attributes (vs instance attributes).
class Pet: max_hunger = 10is shared by all pets.self.hungeris per-instance. Try both. - More methods.
Pet.celebrate_birthday(),Pet.rename(new_name),Pet.adopt_friend(other_pet). Get creative. - A
__str__method — special method that defines whatprint(pet)shows. Makes your class friendlier to print. __repr__— like__str__but for the developer.- Multiple classes interacting — pets and treats, players and enemies, books and authors. Whatever you can imagine.
- Inheritance — make a
Dogclass that’s a special kind ofPet. We’re not formally covering this but if you’re curious:class Dog(Pet):.
What’s next
Next week we start Git — the most important tool in modern programming for saving your code’s history. You’ll finally have a way to undo mistakes, track changes over time, and (eventually, in Phase 6) share your code with the world.
Session 5: Git — saving versions of your code
Session 5: Git — saving versions of your code
Phase 4 — Intermediate Python · Session 5 of 9
What we’re learning today
Today we meet Git — the most important tool in modern programming after the language itself. Git is a tool that saves versions of your code as you work. Every time you make a change worth keeping, you commit it. Later, if you break something, you can go back. If you want to know what changed yesterday, Git tells you. By the end of class, you’ll have a project with Git tracking it, several commits in its history, and the ability to see exactly what changed at each step.
You’ll need to remember from last time
- The terminal — this entire session is in the terminal.
cd,ls,python script.py. - Multi-file projects — Git tracks folders of files.
- Editing files — your normal Thonny workflow.
Part A: What Git is and your first commit
What Git is
Git is version control — a system for saving snapshots of your project as it changes over time. Think of it like the “undo” history in a word processor, but way more powerful:
- Save snapshots whenever you want, with a message describing what you changed.
- Look back at any past snapshot — see what your code looked like a week ago.
- Compare two snapshots to see exactly what changed.
- Recover from mistakes — undo bad changes by going back to a known-good snapshot.
Git is what every professional programmer uses, every day, on every project. It’s not optional in the real world — and once you’ve used it, you’ll never want to work without it.
In this session and the next, we use Git locally — just on your own machine, just for you. In Phase 6 we’ll connect to GitHub (the website where programmers share Git projects), but that’s not today.
One-time setup
Git needs to know who you are so it can label your commits. Open a terminal and run these two commands (with your real name and any email — for local-only Git, the email doesn’t have to be real, but use one consistently):
$ git config --global user.name "Your Name"
$ git config --global user.email "you@example.com"
You only do this once, ever, on this machine. Git remembers.
To check it worked:
$ git config --global user.name
Your Name
$ git config --global user.email
you@example.com
(If your name was already set by a parent or a previous class, running these commands again just updates the value. No harm done. Git is happy to be told what its name is over and over.)
Create a project to track
Open a terminal. Make a new folder for today’s work:
$ cd ~ # go home
$ mkdir version_practice
$ cd version_practice
$ pwd
/home/your_username/version_practice
Create a Python file. Open Thonny, type something simple,
save it as greet.py in the new folder:
# greet.py
name = input("What's your name? ")
print(f"Hello, {name}!")
Save. Confirm in the terminal:
$ ls
greet.py
The folder has one file.
git init — start tracking
Tell Git to start tracking this folder:
$ git init
Initialized empty Git repository in /home/.../version_practice/.git/
Git created a hidden folder called .git inside your
project. That’s where it’ll store all the snapshots and
history. Don’t touch the .git folder. Git manages it.
$ ls -la
total 16
drwxrwxr-x 3 you you 4096 Apr 25 14:00 .
drwxr-xr-x 4 you you 4096 Apr 25 14:00 ..
drwxrwxr-x 7 you you 4096 Apr 25 14:00 .git
-rw-rw-r-- 1 you you 76 Apr 25 14:00 greet.py
The .git folder shows up only with ls -la (because it’s
hidden). It’s the entire Git database for this project.
This folder is now a Git repository (or “repo” for short). Every project tracked by Git is a Git repository.
git status — what’s going on?
The most-used Git command. Run it any time to see what Git thinks about your files:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
greet.py
nothing added to commit but untracked files present (use "git add" to track)
Translation: “You’re on the main branch. You have one file
that Git knows exists but isn’t tracking yet. To track it,
use git add.”
Notice Git is helpful — it tells you what to do next.
git add — stage a file for committing
Tell Git you want to include greet.py in the next snapshot:
$ git add greet.py
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: greet.py
Now greet.py is staged — ready to be included in a
commit. It’s not committed yet; just prepared to be.
The “staging area” is one of Git’s quirks. Files go through three states:
- Working tree — the actual files you’re editing.
- Staging area — files you’ve said “include this in the next snapshot.”
- Committed — saved as part of the project’s history.
git add moves files from working tree to staging.
git commit — save the snapshot
$ git commit -m "Initial version of greet program"
[main (root-commit) abc1234] Initial version of greet program
1 file changed, 2 insertions(+)
create mode 100644 greet.py
You just made your first commit! Git saved a snapshot of your project at this moment.
The -m "..." is the commit message — a short
description of what changed. Always include one. Future-you
(or any teammate) will read these messages to understand
the project’s history.
Good commit messages. Describe what changed in the present tense. “Add greet program,” “Fix typo in instructions,” “Make name input case-insensitive.” Short and specific.
Now check status again:
$ git status
On branch main
nothing to commit, working tree clean
Translation: “Your working tree matches the latest commit. Nothing has changed since.”
Checkpoint: You’ve initialized a Git repository, added a
file, made your first commit, and seen git status confirm
that everything’s clean. This is the natural stop point if
class is cut short.
Part B: Iterate and track changes
A single commit doesn’t show much. Let’s make several commits over time and watch the history grow.
Make a change
Open greet.py in Thonny and add a feature — maybe ask for
the user’s mood:
# greet.py
name = input("What's your name? ")
mood = input("How are you feeling? ")
print(f"Hello, {name}! Glad you're feeling {mood}.")
Save.
git status again
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: greet.py
no changes added to commit but untracked files present (use "git add" to track)
Git noticed greet.py changed. It’s “modified” but not
staged.
git diff — see what changed
$ git diff
diff --git a/greet.py b/greet.py
index abc1234..def5678 100644
--- a/greet.py
+++ b/greet.py
@@ -1,2 +1,3 @@
name = input("What's your name? ")
-print(f"Hello, {name}!")
+mood = input("How are you feeling? ")
+print(f"Hello, {name}! Glad you're feeling {mood}.")
This is a diff — a view of the differences between the
file as it is now and the file as it was in the last commit.
Lines starting with - were removed; lines starting with +
were added. Lines without those marks were unchanged.
git diff is one of the most-used Git commands once you’re
working on real projects. “What did I change since last
commit?” → git diff.
Commit the change
$ git add greet.py
$ git commit -m "Ask the user about their mood"
[main def5678] Ask the user about their mood
1 file changed, 2 insertions(+), 1 deletion(-)
Now you have two commits in your history.
Make a few more changes
Iterate a few times. Each time:
- Make a change in Thonny.
- Save.
git statusto see what changed.git diffto see the actual differences.git addto stage.git commit -m "..."with a clear message.
Examples of changes you could make:
- Add a third question (favorite color, favorite food).
- Validate the input (no empty answers).
- Print multiple lines of output.
- Use
if/elseto give different greetings based on mood.
Make at least three more commits. You’ll have a real history to look at.
git log — see the history
$ git log
commit def567...
Author: Your Name <you@example.com>
Date: Wed Apr 25 14:32:18 2026
Add a third question
commit abc234...
Author: Your Name <you@example.com>
Date: Wed Apr 25 14:25:00 2026
Ask the user about their mood
commit 123abc...
Author: Your Name <you@example.com>
Date: Wed Apr 25 14:20:00 2026
Initial version of greet program
Each commit shows:
- A unique ID (the long hex string)
- Who made it (you)
- When
- The message you wrote
Newest commits are at the top.
A useful one-line variant:
$ git log --oneline
def5678 Add a third question
abc2345 Ask the user about their mood
123abc4 Initial version of greet program
Compact view. Just the IDs (shortened) and messages. Most useful for quick “what’s in this project’s history?”
That’s the base goal for today — a project tracked by Git, with several commits showing the project’s evolution over time.
Stretch — log with diffs
git log -p shows each commit with its diff:
$ git log -p
Press space to scroll, q to quit. Now you can see
every change in every commit.
Stretch — .gitignore
Some files shouldn’t be tracked by Git — temporary files,
caches, secrets. Create a file called .gitignore in your
project folder:
# .gitignore — files Git should ignore
__pycache__/
*.pyc
secrets.txt
Each line is a pattern. __pycache__/ ignores a folder Python
sometimes creates. *.pyc ignores compiled Python files.
secrets.txt ignores a specific file.
Now git status won’t bug you about those files even if
they exist in your folder.
Most real projects have a .gitignore. The
github.com/github/gitignore
repo has templates for common languages.
Extension — track an existing project
Pick a project from a previous session (your text adventure, your hangman, anything). Initialize Git in its folder. Make a first commit (“Existing project as of today”). Then make a small improvement, commit. Make another, commit.
Now even your old projects have version history. From now on, every project you work on can use Git.
Wrap-up
Before we leave, share with the room:
- For the kids who built the greet program — what’s the weirdest commit message you wrote?
- For the kids who used
git diff— was it useful to see exactly what you changed? - For the kids who tried
.gitignore— what did you ignore?
You learned today the foundation of professional version control. Every commit is a permanent snapshot. You can always go back. You can always see what changed. You can work confidently because mistakes are recoverable.
The four commands you’ll use the most:
git status— what’s going on?git add filename— stage a filegit commit -m "message"— save the snapshotgit log --oneline— see the history
Memorize that loop. You’ll use it every day.
Next week we go further — recovering from mistakes, looking at old versions, and lightly touching branches.
If you missed this session
You need a terminal and Git installed (it’s pre-installed on your class machine). Then:
-
Run the one-time setup:
$ git config --global user.name "Your Name" $ git config --global user.email "you@example.com" -
Create a folder, put a Python file in it.
-
In the folder:
$ git init $ git status $ git add yourfile.py $ git commit -m "Initial version" -
Make some changes, repeat steps 2-4 a few times.
-
Run
git log --onelineto see your history.
About 40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
git log --graphshows commits in a visual tree.git show <commit-id>shows the details of a specific commit (use the short ID fromgit log --oneline).git diff --stagedshows what’s about to be committed (already staged but not yet committed).git commit -am "message"combinesgit addandgit commitfor already-tracked files (won’t add new files though).- Aliases — Git lets you create shortcuts.
git config --global alias.s statusmakesgit smeangit status.
What’s next
Next week we use Git for the things people really care about: recovering from mistakes, looking at old versions, and using branches to try out experiments without breaking your main code. The full Git workflow.
Session 6: Git in practice — recovering and exploring
Session 6: Git in practice — recovering and exploring
Phase 4 — Intermediate Python · Session 6 of 9
What we’re learning today
Last week you learned the core Git workflow: edit, add, commit. Today we tackle the real questions: how do I undo a mistake?, how do I see what my code looked like last week?, and how do I try out an experiment without breaking my main code? By the end of class, you’ll have recovered a “broken” file from a previous commit and used a branch to safely experiment with code changes.
You’ll need to remember from last time
git status— what’s going on right now.git add+git commit -m "message"— save a snapshot.git log— see the history.git diff— see what changed since last commit.- The three states — working tree, staging, committed.
Part A: Undoing mistakes
The most-asked question by new Git users: “I made a change. It’s bad. How do I undo it?”
The answer depends on where the change is — working tree, staging, or committed. Different undo tools for each.
Set up: the project
Open a terminal. Find your version_practice folder from
last week (or make a fresh one with git init and a
greet.py file with a few commits — same setup as last
week).
Make sure git status shows “working tree clean”:
$ cd ~/version_practice
$ git status
On branch main
nothing to commit, working tree clean
We start clean.
Scenario 1: I edited a file and want to throw away the changes
Open greet.py in Thonny. Make a deliberately bad change —
delete a line, add some garbage, whatever:
# greet.py
name = input("What's your name? ")
GARBAGE GARBAGE GARBAGE
print(f"Hello, {name}!")
Save. Now git status:
$ git status
On branch main
Changes not staged for commit:
modified: greet.py
You changed it but haven’t staged or committed. To throw away these changes and go back to the last committed version:
$ git restore greet.py
Open the file in Thonny. The garbage is gone. The file is back to what it was at the last commit.
Be careful with
git restore(or its older cousingit checkout -- file). It throws away your changes permanently. There’s no undo. Use it only when you’re sure you want to lose the changes.
Scenario 2: I staged a change but want to unstage it
Make a small change to greet.py and stage it:
$ git add greet.py
$ git status
Changes to be committed:
modified: greet.py
Wait — you wanted to stage something else first. To unstage without losing the change:
$ git restore --staged greet.py
$ git status
Changes not staged for commit:
modified: greet.py
The change is still there in your working tree, but no
longer in staging. Now you can stage what you actually
wanted, or git restore greet.py to throw away the change
entirely.
Scenario 3: I committed something, want to back up
Hardest case. You already committed. Two main options:
a) Make a new commit that undoes the last one — safest:
$ git revert HEAD
This creates a new commit that undoes whatever the most recent commit did. The history shows both the original and the undo. Safe because you don’t lose any history.
HEAD is Git’s name for “the most recent commit.” HEAD~1
is “one before that,” HEAD~2 is “two before that,” etc.
b) Actually delete the last commit — risky:
$ git reset --hard HEAD~1
This removes the most recent commit and resets your files to the previous one. Like the commit never happened.
git reset --hardis dangerous. It loses commits and any uncommitted work. Only use when you’re sure. For most situations,git revertis safer.
Recovering an old version of a file
Sometimes you don’t want to undo a commit — you just want to get the old version of one specific file:
$ git log --oneline greet.py
def5678 Add mood question
abc1234 Initial version
To pull greet.py from a specific commit into your working
tree:
$ git restore --source=abc1234 greet.py
Now greet.py matches what it was in commit abc1234. The
file is changed in your working tree (you’d git status,
then git add and git commit to make it official).
This is useful when you’ve been refactoring for hours and want to start over from a known-good state.
Checkpoint: You’ve used git restore to undo at least
one type of change (working tree, staged, or pulling an old
version of a file). This is the natural stop point if
class is cut short.
Part B: Branches — alternate timelines
Branches let you experiment without affecting your main code. You make a branch, mess around on it, and either keep the changes or throw them away — without touching the main version.
This sounds abstract; the demo makes it concrete.
What’s a branch?
You’ve actually been on a branch the whole time. Run:
$ git branch
* main
You’re on the main branch. The asterisk shows which one.
A branch is a named line of commits — a separate history
that diverged from another branch at some point. By default,
all your work has been on main.
Make a new branch
Suppose you want to try a big change to greet.py but don’t
want to mess up your main version. Make a branch to try it
on:
$ git branch experiment
$ git branch
experiment
* main
You created a branch called experiment but you’re still on
main. Switch to it:
$ git checkout experiment
Switched to branch 'experiment'
$ git branch
* experiment
main
Now you’re on experiment. Any commits you make here go to
the experiment branch, not main.
(There’s a shortcut: git checkout -b experiment creates and
switches to a new branch in one step.)
Make experimental changes
Open greet.py in Thonny. Make a wild change — translate
the prompts to pirate-speak, or add a song lyric, or ask
seventeen questions, or whatever:
# greet.py — experimental pirate version
name = input("Yarrr! What be yer name? ")
print(f"Ahoy, {name}! Welcome aboard, matey!")
Save. Status:
$ git status
On branch experiment
Changes not staged for commit:
modified: greet.py
You’re on experiment. Add and commit:
$ git add greet.py
$ git commit -m "Pirate-themed greeting"
Switch back to main
Now switch to main:
$ git checkout main
$ git branch
experiment
* main
Open greet.py in Thonny. Your pirate changes are gone.
The file is back to what it was on main. Your experiment
is safe on the experiment branch — but main is unchanged.
This is the magic of branches. Each branch has its own files. Switching branches changes what’s in your working tree.
Switch back to experiment
$ git checkout experiment
Open greet.py. The pirate version is back.
Switch to main, switch to experiment, switch back. The file changes each time. Two parallel “alternate timelines” of your project, each with its own state.
Decide what to do
Two outcomes:
You like the experiment and want it on main:
$ git checkout main
$ git merge experiment
git merge experiment brings the experiment branch’s
changes into main. Now main has the pirate version too.
The experiment “succeeded” and got promoted.
You hate the experiment and want to abandon it:
$ git checkout main
$ git branch -D experiment
git branch -D experiment deletes the experiment branch.
Its commits are gone. main is untouched.
(Note the capital -D. Lowercase -d only deletes branches
that have already been merged. Capital -D deletes anything,
including unmerged work. Use carefully.)
Try the full cycle
Set up another experiment. This time, pick a real improvement you’d want to make to your program. Maybe:
- Add input validation
- Try a different greeting style
- Add a
try/exceptfor robustness - Refactor into functions
Branch:
$ git checkout -b improve-greeting
Make your changes. Commit them on the branch. Test them.
Like the result? Merge to main:
$ git checkout main
$ git merge improve-greeting
$ git branch -d improve-greeting
Don’t like it? Throw it away:
$ git checkout main
$ git branch -D improve-greeting
That’s the base goal — you’ve made a branch, made changes on it, and either merged or discarded.
Stretch — visualize branches
git log --oneline --graph --all shows all branches as a
visual tree:
$ git log --oneline --graph --all
* abc1234 (experiment) Pirate-themed greeting
| * def5678 (HEAD -> main) Add mood question
|/
* 123abcd Initial version
The lines and asterisks show the branching. Useful when you have multiple branches.
Extension — branches for daily work
Real workflow: don’t work directly on main. For every
feature or experiment, make a branch. Work there. When it’s
done and tested, merge to main.
This keeps main always in a working state. Try it for a
week:
- Need to add a feature?
git checkout -b add-feature-name. - Done? Test, then merge to main.
- Discovered a bug?
git checkout -b fix-bug-name. Fix it. Test. Merge.
Real teams use this exact pattern (often called “feature branch workflow”). You’re using professional practices.
Wrap-up
Before we leave, share with the room:
- For the kids who used
git restore— was it scary to throw away changes? Did it work as expected? - For the kids who made a branch — was it weird to see the file change when you switched branches?
- For the kids who merged — did the merge work the first time, or did anything go wrong?
You learned today the safety net of professional programming. Mistakes are recoverable. Experiments are isolated. The main version is always intact unless you deliberately change it. This is what gives professional developers the confidence to try things — they know the worst case is “throw it away.”
Three big takeaways:
git restoreundoes uncommitted changes (use carefully).git revertsafely undoes a committed change (creates a new commit; doesn’t lose history).- Branches let you experiment without affecting
main.
Combined with last week’s commit basics, that’s enough Git to handle most real-world situations.
If you missed this session
You need a Git repository (your greet project from last
week, or a fresh one). Then:
-
Make a change to a file. Try
git restore filenameto throw it away. -
Make another change,
git addit. Trygit restore --staged filenameto unstage. -
Make and commit a change you’ll want to undo. Try
git revert HEADto create an undoing commit. -
Make a branch with
git checkout -b experiment. Make changes, commit. Switch back withgit checkout main. Notice the file changes. -
Either merge with
git merge experimentor delete withgit branch -D experiment.
About 40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
git stash— temporarily set aside uncommitted changes. Useful when you need to switch branches but have unsaved work.- Conflict resolution — when you merge two branches that changed the same lines, Git sometimes can’t auto-merge. You have to resolve the conflict by hand. Genuinely tricky; a real-world Git skill.
.gitconfigaliases —git config --global alias.co checkoutmakesgit comeangit checkout. Tons of good aliases on the internet.git blame filename— shows who last changed each line of a file (and in which commit). Useful for “wait, who wrote this code?”git tag v1.0— name a specific commit (like a release). Tags are fixed labels; branches move as you commit.
What’s next
Next week we learn about testing — assert statements
and simple test functions that verify your code does what
you think. After that, you’ll plan and build your milestone
project using all the Phase 4 tools — multi-file structure,
Git tracking, tests verifying your work. Real
software-engineering muscle.
Session 7: Testing — making sure your code works
Session 7: Testing — making sure your code works
Phase 4 — Intermediate Python · Session 7 of 9
What we’re learning today
How do you know your code works? You can run it once and see — but what about next week, after you’ve changed something? What about the edge cases you didn’t think to test? Today we learn automated tests — code that checks other code. By the end of class, you’ll have a small program with its own test file that catches bugs automatically.
You’ll need to remember from last time
- Multi-file programs —
tools.pyandmain.pyimported from each other. - Functions with return values (Phase 3 Session 6).
- Git — we’ll commit our tests alongside our code.
assert— we touched it briefly in Session 6 of Phase 2 and Session 12 of Phase 3 stretches; today it becomes central.
Part A: assert — the simplest test
Open Thonny and create a new folder for today. Save it as
testing_practice. Inside, create math_tools.py:
# math_tools.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def double(x):
return x * 2
Save.
Now create test_math_tools.py in the same folder:
# test_math_tools.py
from math_tools import add, subtract, double
assert add(2, 3) == 5
assert add(0, 0) == 0
assert add(-1, 1) == 0
assert subtract(10, 3) == 7
assert subtract(5, 5) == 0
assert double(4) == 8
assert double(0) == 0
assert double(-3) == -6
print("All tests passed!")
Save. From a terminal, in this folder:
$ python test_math_tools.py
All tests passed!
You just ran your first tests.
What assert does
assert condition checks if a condition is true. If it’s
true, nothing happens (the line just runs). If it’s false,
the program crashes immediately with an
AssertionError.
So assert add(2, 3) == 5 says: “I claim that add(2, 3)
returns 5. If not, something is wrong — crash.”
When all assertions pass, you reach the print("All tests passed!") line. When one fails, the program crashes
before that line, telling you which assertion failed.
Watch a test fail
Open math_tools.py and break it on purpose:
def add(a, b):
return a - b # typo! should be a + b
Save. Run the tests:
$ python test_math_tools.py
Traceback (most recent call last):
File "test_math_tools.py", line 4, in <module>
assert add(2, 3) == 5
AssertionError
The very first assertion failed. Python told you exactly
which line — assert add(2, 3) == 5 — meaning add(2, 3)
did not return 5.
Fix the typo (back to a + b). Run again. All pass.
This is the magic: the test caught the bug. You didn’t have to manually check each function — running the test file did it for you. And next time you change the code, you can run the tests again to make sure you didn’t break anything.
Test functions for organization
When you have lots of tests, group them into functions:
# test_math_tools.py
from math_tools import add, subtract, double
def test_add():
assert add(2, 3) == 5
assert add(0, 0) == 0
assert add(-1, 1) == 0
print("add tests passed")
def test_subtract():
assert subtract(10, 3) == 7
assert subtract(5, 5) == 0
print("subtract tests passed")
def test_double():
assert double(4) == 8
assert double(0) == 0
assert double(-3) == -6
print("double tests passed")
# run all tests
test_add()
test_subtract()
test_double()
print("\nAll tests passed!")
Each test function checks one thing. The bottom of the file calls them all in turn.
When something breaks, the print statements tell you which group is the last one that worked — the broken function is the next one. Useful narrowing-down.
Checkpoint: You have a code file (math_tools.py) and
a test file (test_math_tools.py) that runs assertions
against the code, and you’ve seen at least one assertion
fail when you broke the code. This is the natural stop
point if class is cut short.
Part B: Test a real project
Now we’ll write tests for something more substantial — either the Pet class from Session 4, or a project of your choice.
Recreate the Pet class (or use yours)
In your folder, create pet.py:
# pet.py
class Pet:
def __init__(self, name):
self.name = name
self.hunger = 5
def feed(self, amount):
self.hunger = self.hunger - amount
if self.hunger < 0:
self.hunger = 0
def is_hungry(self):
return self.hunger > 3
Save.
Now create test_pet.py:
# test_pet.py
from pet import Pet
def test_new_pet():
pet = Pet("Rex")
assert pet.name == "Rex"
assert pet.hunger == 5
print("test_new_pet passed")
def test_feed_reduces_hunger():
pet = Pet("Rex")
pet.feed(2)
assert pet.hunger == 3
print("test_feed_reduces_hunger passed")
def test_feed_never_below_zero():
pet = Pet("Rex")
pet.feed(100)
assert pet.hunger == 0
print("test_feed_never_below_zero passed")
def test_hungry_check():
pet = Pet("Rex")
assert pet.is_hungry() == True # starts with hunger=5, > 3
pet.feed(2)
assert pet.is_hungry() == False # hunger is now 3, not > 3
print("test_hungry_check passed")
test_new_pet()
test_feed_reduces_hunger()
test_feed_never_below_zero()
test_hungry_check()
print("\nAll tests passed!")
Run:
$ python test_pet.py
test_new_pet passed
test_feed_reduces_hunger passed
test_feed_never_below_zero passed
test_hungry_check passed
All tests passed!
Walk through what’s new:
- Each test function tests one specific thing. Pet starts with the right state. Feed reduces hunger. Feed doesn’t go below zero. The hungry check works correctly.
- Each test creates its own pet. Fresh start; no interference between tests.
- Tests both happy and edge cases.
feed(2)is normal;feed(100)is “what if someone overfeeds?”
The fourth test is interesting — it tests is_hungry()
after a state change, ensuring the method updates correctly.
Find a bug
Now break pet.py deliberately:
def is_hungry(self):
return self.hunger >= 3 # changed from > to >=
Run the tests:
$ python test_pet.py
test_new_pet passed
test_feed_reduces_hunger passed
test_feed_never_below_zero passed
Traceback (most recent call last):
File "test_pet.py", line 28, in <module>
test_hungry_check()
...
AssertionError
The change broke test_hungry_check. The tests caught it
immediately. Without tests, you might have spent hours
wondering why your game felt slightly off (“the pet seems
hungry too easily”).
Fix it (back to >) and tests pass again.
That’s the base goal.
Stretch — a run_tests helper
Wrap the test-running into a helper that catches errors and reports cleanly:
def run_tests(test_functions):
passed = 0
failed = 0
for test in test_functions:
try:
test()
passed = passed + 1
except AssertionError:
print(f"FAIL: {test.__name__}")
failed = failed + 1
print(f"\n{passed} passed, {failed} failed")
run_tests([
test_new_pet,
test_feed_reduces_hunger,
test_feed_never_below_zero,
test_hungry_check,
])
Now even if a test fails, the others keep running, and you get a summary at the end:
4 passed, 0 failed
Or if there’s a problem:
FAIL: test_hungry_check
3 passed, 1 failed
test.__name__ is a Python feature — every function has a
.__name__ attribute that gives you its name as a string.
Useful for error messages.
Extension — assertion messages
Add a message to assert so failures are more
self-explanatory:
assert pet.hunger == 0, f"After overfeeding, hunger should be 0 but was {pet.hunger}"
The string after the comma is shown when the assertion fails:
AssertionError: After overfeeding, hunger should be 0 but was -95
Way more useful than just AssertionError. Real testing
frameworks (like pytest) build on this idea.
Extension — pytest
Real Python testing uses a tool called pytest. Install it:
$ pip install pytest
Then test files written in pytest’s style:
# test_pet_pytest.py
from pet import Pet
def test_new_pet():
pet = Pet("Rex")
assert pet.name == "Rex"
assert pet.hunger == 5
def test_feed_reduces_hunger():
pet = Pet("Rex")
pet.feed(2)
assert pet.hunger == 3
# ... more tests ...
No print statements; no run_tests call at the bottom.
Just functions starting with test_. Then run with
pytest:
$ pytest
====== test session starts ======
collected 4 items
test_pet_pytest.py .... [100%]
====== 4 passed in 0.01s ======
pytest finds all functions starting with test_ and runs
them. Reports clearly. Has tons of features for bigger
projects.
We’re using basic assert today because it’s built-in.
pytest is what real projects use; mention to anyone
curious.
Wrap-up
Before we leave, share with the room:
- For the kids who tested the Pet class — did you find any bugs?
- For the kids who broke their code on purpose — did the tests catch it?
- For the kids who tried pytest — was it easier or harder?
You learned today the discipline that separates casual code from professional code. Code with tests is trustworthy. When you change something, you can re-run the tests and know in seconds whether you broke anything else. Without tests, you have to manually check everything every time — which means you don’t, and bugs sneak through.
Real projects have thousands of tests. They run automatically every time someone commits code. If a test fails, the change is rejected. This is how huge projects stay reliable.
The next two sessions are your milestone project — this time, with multi-file structure, Git from day one, and tests for the important parts. Real software-engineering practice.
If you missed this session
Open Thonny and create a folder. Inside it:
-
Make
math_tools.pywith a few simple functions (add,subtract,double). -
Make
test_math_tools.pywithassertstatements testing each function. -
Run from the terminal:
python test_math_tools.py. See the “All tests passed” message. -
Break one of your functions on purpose. Run the tests. See the failure. Fix it. Run again.
-
Group tests into functions and call them from the bottom of the file.
About 35 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
pytest— the real Python testing tool.pip install pytestthen run withpytest. Used by every major Python project.- Test edge cases — empty inputs, very large inputs, negative numbers, zero. Real bugs hide in edge cases.
- Test that exceptions are raised — sometimes you want
to verify that bad input does error. With pytest:
import pytest def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(10, 0) - Test-driven development — write tests first, before the code. Code passes when all tests pass. Real practice.
What’s next
Next week is the start of your milestone project for Phase 4. This time, you’ll use multi-file structure from day one, Git from day one, and at least a few tests for the important pieces. You’re not just writing code anymore — you’re doing software engineering.
Session 8: Milestone project work day 1
Session 8: Milestone project work day 1
Phase 4 — Intermediate Python · Session 8 of 9
What we’re learning today
Today is your day. You’ll plan a Python project that uses the Phase 4 toolkit — multi-file structure, Git tracking, maybe classes, maybe tests — and start building it. Next week you’ll finish it and demo it to the class.
You’ll need to remember from last time
- Multi-file programs (Session 2) — split your code across files.
- Stdlib modules (Session 3) —
datetime,pathlib,sys,os, etc. - Classes (Session 4) — bundle data and methods.
- Git (Sessions 5-6) —
init,add,commit,log, branches. - Tests (Session 7) —
assertstatements that catch bugs.
Part A: Plan your project
Same shape as previous milestones, with new requirements specific to Phase 4.
The plan
Take a piece of paper or open a blank text file. Answer these six questions:
-
What’s the program? (One sentence.)
-
What does the user do? (How do they interact? Input? Subcommands? Menu?)
-
What files will you have? (Brainstorm. At least two — one for the main program, one for a module. Could be more. Examples:
main.py,data.py,widgets.py.) -
Will you use classes? (Optional. If yes, what are they?)
-
What’s the simplest version? (Build this FIRST.)
-
What will you test? (At least one or two
asserttests for the important pieces.)
If you don’t have an idea
Pick one and modify:
- A multi-file CLI tool — extended journal, todo list,
budget tracker, contact book. Use
sys.argvfor subcommands. - A class-based simulation — extended Pet game with multiple Pet types; a small bank with multiple accounts; a mini RPG with players and enemies.
- A tested utility library — a module of useful functions (string utilities, math helpers, date utilities) with thorough tests.
- A multi-file game — text adventure with rooms in their own file, items in another, game logic in main.
- A “learning tool” — flashcards, quiz, a math practice app, anything educational.
Pick one. Spend two minutes. Don’t overthink.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question.
Set up the project — with Git from day one
Create a folder for your project. In a terminal:
$ cd ~ # or wherever you keep projects
$ mkdir my_project # use your project's name
$ cd my_project
$ git init # start tracking immediately
Make your first file (probably main.py). Commit it before
writing much:
$ git status
$ git add main.py
$ git commit -m "Initial project setup"
Commit early, commit often. Every meaningful change gets its own commit. Don’t wait until the end.
Build the simplest version first
Look at your answer to question 5. Build that first. Iterate from there.
For each significant change:
- Make the change.
- Test it (run the code, make sure it works).
- Commit (
git add ...; git commit -m "..."). - Move to the next thing.
If something feels risky (a big change you might want to undo), make a branch:
$ git checkout -b try-new-thing
# ... experiment ...
$ git checkout main
$ git merge try-new-thing # if it worked
# OR
$ git branch -D try-new-thing # if it didn't
This is real software engineering practice.
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today.
Bring your project (just the folder — Git carries the history) next week. We’ll finish, then demo.
If you missed this session
Open a terminal. Then:
-
Spend 10-15 minutes answering the six planning questions.
-
Create a folder for your project.
git initin it. Create your first file. Commit. -
Build the simplest version. Commit as you go (every few meaningful changes).
About 50 minutes total — planning + significant build time.
If you don’t have an idea, the seed list above is a starting point. Pick one and modify.
Stretch and extension ideas
If your base project is working and you want to add more:
- Add tests for your core functions/methods (Session 7).
- Use a branch to try a risky feature without breaking main (Session 6).
- Read configuration from a file instead of hardcoding (Session 11 of Phase 3).
- Add CLI subcommands (Session 3 — git/journal pattern).
- Use a class to organize state (Session 4).
- Use a stdlib module you haven’t used yet (
json,csv,datetime).
Whatever you add, commit each change. Future you wants to see the journey.
What’s next
Next week you’ll have time to finish, polish, and demo your project to the class. Each person gets 3-5 minutes. Bring a working project, your enthusiasm, and your Git log — part of the demo will be showing the journey of how your project came together.
Session 9: Milestone project work day 2 + demo day
Session 9: Milestone project work day 2 + demo day
Phase 4 — Intermediate Python · Session 9 of 9
What we’re learning today
Today is the last session of Phase 4. You’ll spend the first half finishing your project — fixing bugs, adding the last features, making sure your tests pass. Then you’ll demo to the class. By the end, you’ll have shipped your fourth milestone project, this one with multi-file structure, Git history, and (hopefully) tests. You’re ready for Phase 5.
You’ll need to remember from last time
- Your project plan from last week.
- Whatever you got working last week.
- Your buddy for testing.
- Git — commit your final changes today.
Part A: Final polish
You have about 30 minutes to finish.
What “finished” means
Same three rules as always:
- It runs without crashing under normal use.
- It does what your plan said it would do — at least the simplest version.
- You can explain how it works.
If your project doesn’t meet all three, focus on getting there before adding anything new.
Polish ideas (Phase 4 specific)
- Run your tests. Make sure they all pass.
- Add a test or two for anything critical that doesn’t have one.
- Clean up your commit messages. Look at your
git log. Are the messages clear? They don’t have to be perfect, but they should describe what changed. - Make sure all your files are committed.
git statusshould say “working tree clean” before the demo. - Add a README — a
README.mdfile with one paragraph about what your project does and how to run it. Every real project has one.
Buddy test
About 15 min in, swap with your buddy. They use your program for a few minutes; you use theirs. Notice and report.
Checkpoint: Your project runs the basic version of
what you planned, and your git log shows the journey of
how you built it. This is the natural stop point if
class is cut short — but today, demo time is next.
Part B: Demo day
Each person gets 3-5 minutes. Same format as before, plus a Phase 4 twist: show your Git log.
How a demo works
When it’s your turn:
- Show your project. Run it. Use it.
- Tell us about it. What is it? What does it do?
- Show your Git log. Run
git log --oneline. Walk us through how the project came together — what was the first commit? What was the last? What was a turning point? - Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected.
- Take one question.
The Git log section is new. Your project’s history is part of the project. Showing how you got here matters as much as what you got to.
After everyone demos
Mr. Eric will say a few specific things about each project. Then we’re done — Phase 4 is complete.
What you accomplished
- You shipped your fourth milestone project. Most adults have shipped zero. You’re at four.
- You used multi-file structure — your project is organized like real software.
- You used Git from day one — you have a real version history of your work.
- (If you wrote tests) You verified your code with automated tests.
- You finished Phase 4. The professional toolkit is yours.
What’s next: Phase 5 — customtkinter (desktop apps)
Phase 5 is a shorter, focused phase: building desktop apps with windows, buttons, forms, and other widgets.
What’s different:
- Visual again. Phase 4 was all text. Phase 5 you’ll build apps with actual user interfaces — windows, buttons, text inputs, layouts.
- Heavy class use. Every widget is a class. Phase 4’s light classes intro becomes daily practice.
- A new library:
customtkinter— a modern Python GUI toolkit. Looks much nicer than the old standard (tkinter); same mental model.
After Phase 5, you’ll have built apps that look like real software — apps you could install on someone else’s computer and they’d know how to use.
Bring your machine and your enthusiasm. See you in Phase 5.
If you missed this session
Two cases:
Missed only the demo half: Show your project to Mr. Eric at the start of next week’s class. Same kind of feedback you’d have gotten in the demo.
Missed the whole session: No big deal. Finish your project at home using your Session 8 plan, and bring it next week. Or just join us in Phase 5 — you’ve already shipped enough this phase to be proud.
Stretch and extension ideas
If you have time after demos:
- Polish more. Add features, more tests, better error messages.
- Build a
requirements.txt— a file listing any external Python packages your project needs. (Phase 4 projects mostly don’t need this since we used stdlib only, but it’s a real-project file.) - Write a longer README — installation instructions, examples, future ideas.
- Save your project file. You’ll want them all later.
What’s next
Phase 5 — customtkinter (desktop apps with real windows). See you there.
Phase 5 — customtkinter (desktop apps)
You can write CLI tools. You can save versions of your work with Git. Now we go visual again — building desktop apps with windows, buttons, forms, and other widgets that look like real software.
What this phase is
Eight focused sessions on building GUI (Graphical User Interface) apps with Python. Less new programming syntax, more new ways to think about programs. Programs with GUIs aren’t sequential — they sit there waiting for the user to do something, then react. That’s a different mental model from the text-based programs of Phases 3-4.
By the end, you’ll have built a desktop app of your own design — something with windows, buttons, inputs, and real behavior. The kind of thing you could install on someone else’s computer and they’d know how to use.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to GUIs — your first window | CTk(), CTkLabel, mainloop |
| 2 | Buttons and events | CTkButton + command callbacks |
| 3 | Inputs — entries and text boxes | CTkEntry, text input from the user |
| 4 | Choices — checkboxes, radios, dropdowns | choice widgets |
| 5 | Layouts and frames | grid, CTkFrame for organized UIs |
| 6 | Putting it together — a complete app | guided build of a real app |
| 7 | Milestone project work day 1 | plan + build |
| 8 | Milestone project work day 2 + demo | finish + showcase |
The Python skills here build on Phase 4’s classes (every widget is a class) and multi-file structure (real apps are multi-file). What’s new is the event-driven model — the program waits for the user, then reacts.
What you’ll build
Smaller widgets-and-callbacks experiments throughout the phase, then bigger apps:
- Sessions 1-4: small examples — counters, greeters, forms.
- Session 5: an organized multi-frame layout (a settings panel, a multi-section form, etc.).
- Session 6: a complete app together — a todo list, notes app, or simple calculator.
- Sessions 7-8: your milestone app — your design, your code.
What you’ll need
- The same machine as before. customtkinter is pre-installed on the class machine.
- For working at home, install with:
$ pip install customtkinter - Thonny still works as your editor.
How sessions work
Same shape as before:
- Part A introduces a new widget or concept with a guided exercise.
- Part B is open practice or a project.
- Wrap-up to share what you did.
A note about GUI programs
GUI programs are different from the text programs you’ve been writing. A GUI program sits there waiting for the user to click a button or type something. When the user does, the program reacts (runs a callback function). Then it goes back to waiting.
This is called the event loop, and it’s a fundamentally different mental model. You’re not writing a sequence of steps — you’re describing widgets and what happens when the user interacts with each one.
Phase 1’s Scratch had something similar (the when green flag clicked and when sprite clicked blocks were
events). GUI programming brings that back, this time in real
Python with real windows.
A note about classes
Per Phase 4 Session 4: “every widget is a class.” That’s
literally true here. CTkButton is a class. CTkLabel is
a class. When you write button = ctk.CTkButton(...) you’re
creating an instance, just like Pet from Phase 4.
Your own apps will often also use classes — your code becomes a class that contains widgets. We’ll touch this in later sessions.
Where to start
Session 1: Welcome to GUIs opens your first window.
When you’re stuck, the Getting unstuck appendix has the checklist. The Glossary will grow as Phase 5 introduces new terms.
Welcome to building real desktop apps. Let’s go.
Session 1: Welcome to GUIs — your first window
Session 1: Welcome to GUIs — your first window
Phase 5 — customtkinter · Session 1 of 8
What we’re learning today
Phase 4 was all text. Today we go visual again — but in a different way than Scratch or Turtle. We’re building GUIs (Graphical User Interfaces): apps with windows, buttons, text fields. The kind of thing that looks like a real program when it runs. By the end of class, you’ll have a window appear on your screen with custom text, fonts, and colors — your first real desktop app.
You’ll need to remember from last time
- Classes from Phase 4 Session 4 — every widget you’ll use today is a class.
import—import customtkinter as ctkbrings in the GUI library.- Functions — we’ll use them as callbacks in Session 2.
Part A: Your first window
Open Thonny. Create a new file. Save it as first_gui.py.
The minimum GUI
Type this exactly:
import customtkinter as ctk
app = ctk.CTk()
app.title("My First App")
app.geometry("400x300")
label = ctk.CTkLabel(app, text="Hello, world!")
label.pack(pady=20)
app.mainloop()
Save. Run.
A window opens. Dark background (the default theme), a label that says “Hello, world!” near the top. The window has a title bar saying “My First App.”
That’s a real desktop app — running in its own window, separate from Thonny.
Close the window when you’re done looking at it. (Click the X in the corner.)
What just happened
Walk through line by line:
import customtkinter as ctk— load the library, give it the short namectk. We’ll usectk.a lot today.app = ctk.CTk()— create the main window (also called the “root window”).CTkis a class;CTk()calls the constructor and gives you an instance.app.title("My First App")— set the title bar text.app.geometry("400x300")— set the window size: 400 pixels wide by 300 tall.label = ctk.CTkLabel(app, text="Hello, world!")— create a label widget that lives insideapp.CTkLabelis another class. We pass it two arguments: the parent (where the label lives) and the text to show.label.pack(pady=20)— make the label visible by adding it to the window.packis a layout method — it stacks widgets one after another. Thepady=20adds 20 pixels of vertical padding around it.app.mainloop()— run the event loop. The window appears and stays open until the user closes it. This is the magic of GUI programs.
The event loop
That last line is the most conceptually new thing today.
In all your previous programs, the program ran top to bottom and ended. A loop kept it going if you wanted, but when there was no more code to run, it stopped.
GUI programs are different. After app.mainloop(), the
program sits there waiting. It doesn’t end. It waits for
the user to click something, type something, close the
window. When the user does, the program reacts (runs the
right code), then goes back to waiting.
This is the event loop — the fundamental difference between GUI programs and text programs. You write code that describes the interface and what happens on each event. The event loop handles the rest.
(Phase 1’s Scratch had something similar — the green flag events, the click events. GUI programming brings that back in Python form.)
Customize the window
Try a few changes:
app.title("My Awesome App")
app.geometry("600x400")
Bigger window, different title.
Change the label:
label = ctk.CTkLabel(app, text="Hello from your first GUI!", font=("Arial", 24))
The font=("Arial", 24) makes the text bigger. You can pass
many other options to widgets — color, padding, size. We’ll
see more of them.
Multiple labels
You can add as many widgets as you want. Add another label:
import customtkinter as ctk
app = ctk.CTk()
app.title("My First App")
app.geometry("400x300")
title_label = ctk.CTkLabel(app, text="Welcome!", font=("Arial", 32))
title_label.pack(pady=20)
subtitle = ctk.CTkLabel(app, text="This is my first GUI.", font=("Arial", 14))
subtitle.pack(pady=5)
footer = ctk.CTkLabel(app, text="Made with customtkinter")
footer.pack(pady=10)
app.mainloop()
Run. Three labels stack vertically. pack adds them in the
order you call it — top to bottom by default.
Checkpoint: You have a window with at least two custom labels (different text, different fonts), a custom title, and a custom size. This is the natural stop point if class is cut short.
Part B: Customize and explore
Now make it yours.
Light vs dark theme
By default, customtkinter uses dark mode. You can switch:
ctk.set_appearance_mode("light") # or "dark", or "system"
Add this before creating the window:
import customtkinter as ctk
ctk.set_appearance_mode("light") # NEW
ctk.set_default_color_theme("blue") # also try "green", "dark-blue"
app = ctk.CTk()
# ... rest of the code ...
Try "light", "dark", and "system" (which follows your
OS theme). Try different color themes.
A personal welcome screen
Build a welcome screen for your app — name it after something you’d want to make. Maybe “Caleb’s Quiz Game” or “Bible Verse of the Day” or “Sam’s Drawing Tool.”
Aim for at least four labels:
- A big title.
- A subtitle or description.
- Maybe an emoji or symbol (yes, customtkinter shows emojis in labels).
- A footer or “by you” credit.
Use different fonts and sizes to give it visual hierarchy (big = important, small = footnote).
import customtkinter as ctk
ctk.set_appearance_mode("dark")
app = ctk.CTk()
app.title("Bible Verse of the Day")
app.geometry("500x400")
title = ctk.CTkLabel(
app,
text="📖 Verse of the Day",
font=("Arial", 32, "bold")
)
title.pack(pady=30)
verse = ctk.CTkLabel(
app,
text="\"For God so loved the world...\"",
font=("Arial", 16)
)
verse.pack(pady=10)
reference = ctk.CTkLabel(
app,
text="John 3:16",
font=("Arial", 14, "italic")
)
reference.pack(pady=5)
footer = ctk.CTkLabel(
app,
text="Made by Sam",
font=("Arial", 10)
)
footer.pack(pady=20)
app.mainloop()
Save. Run. Your own little app appears.
This is the base goal. A personal welcome screen with multiple labels, custom fonts, and your own theme.
Stretch — colored labels
Labels can have custom colors:
title = ctk.CTkLabel(
app,
text="Important!",
font=("Arial", 24),
text_color="red"
)
Try different colors. Strings work ("red", "blue",
"#FF5733" for hex codes).
Stretch — backgrounds
Whole sections can have colored backgrounds. Use fg_color:
title = ctk.CTkLabel(
app,
text="Important",
font=("Arial", 24),
fg_color="darkblue",
corner_radius=10,
width=300,
height=60
)
fg_color is the background of the widget itself.
corner_radius makes rounded corners. width and height
set explicit size.
Extension — multiple themes
Add a “what theme do you want?” prompt at the start (using
plain input() — no GUI yet for that since we don’t have
buttons until next week):
import customtkinter as ctk
mode = input("Theme (light/dark/system): ")
ctk.set_appearance_mode(mode)
app = ctk.CTk()
# ... rest ...
Run from the terminal. Type a theme. The window opens with that theme.
This is a tiny taste of “the user controls the appearance” — a real feature of real apps.
Wrap-up
Before we leave, share with the room:
- For the kids who built a personal welcome screen — what’s it for? What did you call it?
- For the kids who tried different themes — which one’s your favorite?
- Did anyone get the window to come up empty (no labels)? What was missing?
You learned today the foundation of every desktop app you’ve ever used. A window. Widgets inside. An event loop that keeps it alive. Microsoft Word, Discord, Spotify, every game launcher — all built on the same shape.
The mental shift is real: GUI programs describe an interface rather than follow a sequence. You’re saying “this widget here, this widget there, here’s what they do.” The event loop runs everything.
If you missed this session
Open Thonny and start a new file. Save it as first_gui.py.
Then:
-
Type the minimum GUI from Part A:
import customtkinter as ctk app = ctk.CTk() app.title("My First App") app.geometry("400x300") label = ctk.CTkLabel(app, text="Hello, world!") label.pack(pady=20) app.mainloop()Save. Run. A window appears.
-
Add more labels with different fonts and sizes.
-
Try
ctk.set_appearance_mode("light")(before creating the window). -
Build a personal welcome screen (Part B base).
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Window icons —
app.iconbitmap("icon.ico")(Windows) or with a.pngfor cross-platform. Need to find or create an icon file first. padyandpadxfor spacing.pady=(20, 5)is asymmetric: 20 pixels above, 5 below. Useful for fine-tuning layout.anchor— controls how a widget aligns in its space.label.pack(anchor="w")sticks to the west (left) side.- Resizable windows — the user can drag the corner to
resize.
app.resizable(False, False)disables resizing. destroy()— programmatically close the window:app.destroy(). Useful inside callbacks (next week).
What’s next
Next week we add buttons — interactive widgets the user can click to make things happen. That’s where the GUI gets alive. Combined with today’s labels, you’ll have apps that respond to the user.
Session 2: Buttons and events
Session 2: Buttons and events
Phase 5 — customtkinter · Session 2 of 8
What we’re learning today
Last week your window just sat there — labels, but nothing to click. Today we add buttons that react when the user clicks them. By the end, you’ll have built a counter app: a button that, when clicked, makes a number go up on the screen. Tiny but real — and the same pattern that powers every interactive app you’ve ever used.
You’ll need to remember from last time
import customtkinter as ctk— the GUI library.ctk.CTk()— main window.ctk.CTkLabel(parent, text="...")— a text label..pack()— add a widget to the window.app.mainloop()— start the event loop.- The event loop — the program waits for the user to do something.
- Functions — we’ll write functions called when the button is clicked.
Part A: Your first button
Open Thonny. Save a new file as button_app.py.
A button that does one thing
import customtkinter as ctk
def say_hello():
print("Hello!")
app = ctk.CTk()
app.title("Button App")
app.geometry("400x200")
button = ctk.CTkButton(app, text="Click me!", command=say_hello)
button.pack(pady=50)
app.mainloop()
Save. Run.
A window opens with a button. Click it. Look at the shell in Thonny — “Hello!” appears. Click again. Another “Hello!”. The button calls the function every time you click.
Walk through what’s new:
def say_hello():— a function. Same as Phase 2-3 functions. No parameters.ctk.CTkButton(app, text="Click me!", command=say_hello)— a button widget. The new parameter iscommand. It takes a function (not a function call — just the name).- The function gets called every time the button is clicked.
command= is special
Notice: command=say_hello, not command=say_hello().
The () would call the function and pass its return
value (which would be None) to command. We don’t want
that. We want to give the button the function itself, so
the button can call it later when it needs to.
This is one of the most common Python beginner mistakes. Pass the function name, not a call.
command=say_hello # CORRECT — pass the function
command=say_hello() # WRONG — calls it now, passes the result
If you ever click your button and nothing happens (or it runs once when the program starts and never again), check this.
Multiple buttons
You can have as many buttons as you want, each with its own function:
import customtkinter as ctk
def say_hello():
print("Hello!")
def say_goodbye():
print("Goodbye!")
def make_a_noise():
print("BEEP BEEP")
app = ctk.CTk()
app.title("Multi-button App")
app.geometry("400x300")
button1 = ctk.CTkButton(app, text="Hello", command=say_hello)
button1.pack(pady=10)
button2 = ctk.CTkButton(app, text="Goodbye", command=say_goodbye)
button2.pack(pady=10)
button3 = ctk.CTkButton(app, text="Make noise", command=make_a_noise)
button3.pack(pady=10)
app.mainloop()
Three buttons. Three functions. Each button calls its own.
Checkpoint: You have a window with at least two buttons, each calling a different function that prints something different. This is the natural stop point if class is cut short.
Part B: A counter app
Printing to the shell is fine, but real apps update the window itself. Let’s build a counter — a number on the screen that goes up when you click a button.
Build it
import customtkinter as ctk
count = 0
def increment():
global count
count = count + 1
label.configure(text=f"Count: {count}")
app = ctk.CTk()
app.title("Counter")
app.geometry("400x200")
label = ctk.CTkLabel(app, text="Count: 0", font=("Arial", 32))
label.pack(pady=20)
button = ctk.CTkButton(app, text="Click me!", command=increment)
button.pack(pady=10)
app.mainloop()
Save. Run. Click the button. The number goes up on the screen. Each click adds 1.
This is what every app does. Counter, score, message display, timer — all variations of “user does something → update the display.”
What’s new
A few things:
count = 0at the top — a regular variable that starts at 0.def increment():— the callback function.global countinside the function — tells Python that when we saycount, we mean the variable up top, not a new local one. Withoutglobal, Python would create a new localcountand the outer one would never change.label.configure(text=...)— this is how you change a widget’s properties after creating it. The.configure()method takes the same parameters as the constructor.
About global
The global count line is a Python wrinkle worth
explaining. By default, when a function assigns to a
variable, Python treats it as a new local variable. So:
count = 0
def increment():
count = count + 1 # This makes a NEW local count
The count = count + 1 line creates a new local variable
called count, sets it to (outer count) + 1, then throws
it away when the function ends. The outer count is never
changed.
To modify the outer count, you need global count:
count = 0
def increment():
global count # "use the outer count"
count = count + 1
This is one of those Python quirks. In bigger programs,
global is considered bad style — better solutions exist
(using classes, or returning values). For a small app, it
works.
(Phase 4 Session 4 covered classes. The “right” way to do this is to wrap the counter in a class. We’ll touch that later.)
Customize the counter
Try variations:
- Add a “Reset” button that sets
countback to 0. - Add a “Decrement” button that goes down.
- Add a “Multiply by 2” button.
- Make the font huge and the buttons big.
That’s the base goal — a counter app with at least three buttons (increment, reset, and one more of your choice).
Stretch — a tiny clicker game
Build a “clicker game” — every click adds points; certain milestones trigger messages:
import customtkinter as ctk
score = 0
def click():
global score
score = score + 1
score_label.configure(text=f"Score: {score}")
if score == 10:
message_label.configure(text="🎉 You hit 10!")
elif score == 50:
message_label.configure(text="🌟 50 points! Amazing!")
elif score == 100:
message_label.configure(text="🏆 100! You're a champion!")
app = ctk.CTk()
app.title("Clicker")
app.geometry("400x300")
title = ctk.CTkLabel(app, text="Click as fast as you can!", font=("Arial", 18))
title.pack(pady=10)
score_label = ctk.CTkLabel(app, text="Score: 0", font=("Arial", 32))
score_label.pack(pady=20)
button = ctk.CTkButton(app, text="CLICK", command=click, font=("Arial", 24), height=60)
button.pack(pady=10)
message_label = ctk.CTkLabel(app, text="", font=("Arial", 14))
message_label.pack(pady=10)
app.mainloop()
Run. Click fast. Hit 10, 50, 100 — different messages appear.
That’s a real little game. Two state variables (score, which message), conditional logic, multiple labels updating. The same pattern scales to bigger games.
Extension — buttons that change buttons
Make a button that changes its own text each time it’s clicked:
import customtkinter as ctk
texts = ["Click me", "Again", "Once more", "Last time", "Click me"]
index = 0
def change_text():
global index
index = (index + 1) % len(texts)
button.configure(text=texts[index])
app = ctk.CTk()
app.title("Changing button")
app.geometry("400x200")
button = ctk.CTkButton(app, text=texts[0], command=change_text)
button.pack(pady=50)
app.mainloop()
The button’s text cycles through a list. Same pattern — state variable + callback that updates a widget — but the widget being updated is the same one being clicked.
Extension — wrap in a class
If you want to do this the “right way” (per Phase 4 Session 4), put everything in a class:
import customtkinter as ctk
class CounterApp:
def __init__(self):
self.count = 0
self.app = ctk.CTk()
self.app.title("Counter (class version)")
self.app.geometry("400x200")
self.label = ctk.CTkLabel(self.app, text="Count: 0", font=("Arial", 32))
self.label.pack(pady=20)
self.button = ctk.CTkButton(self.app, text="Click me!", command=self.increment)
self.button.pack(pady=10)
def increment(self):
self.count += 1
self.label.configure(text=f"Count: {self.count}")
def run(self):
self.app.mainloop()
# create and run
counter = CounterApp()
counter.run()
Notice — no more global. The count lives on the class
instance (self.count). Methods access it via self. This
is real-world structure for non-trivial GUI apps.
We’ll do more of this in later sessions.
Wrap-up
Before we leave, share with the room:
- For the kids who built the counter — what other buttons did you add?
- For the clicker game — did anyone get to 100?
- Did anyone get the “button does nothing” bug? Was it the
command=function()mistake?
You learned today the heart of GUI programming: user does something → callback runs → display updates. That single loop is what every interactive app does. Word processors, games, web pages, mobile apps — all variations of that pattern.
You also met .configure() for changing widgets after
creation. Until now, widgets were fixed once made; now
they can update. That’s how interfaces feel alive.
If you missed this session
Open Thonny and start a new file. Save as button_app.py.
Then:
-
Build the basic button + callback from Part A. Click it; see the shell output.
-
Build the counter app from Part B base. Notice how
label.configure(text=...)updates the display. -
Try adding a Reset button.
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Disable a button —
button.configure(state="disabled")grays it out. Re-enable withstate="normal". - Hide a widget —
label.pack_forget()removes it from view (it still exists; can.pack()it again later). after()— schedule a function to run after a delay.app.after(1000, my_function)runsmy_functionin 1000 milliseconds (1 second). Useful for animations and timers.destroy()— close the window withapp.destroy(). Call it inside a callback for a “Quit” button.- Lambda functions —
command=lambda: print("hi")defines a tiny one-line function inline. Useful when the callback is trivial.
What’s next
Next week we add input widgets — text fields the user can type into. Combined with today’s buttons, you’ll be able to build apps that take real text input and respond to it. Real forms!
Session 3: Inputs — entries and text boxes
Session 3: Inputs — entries and text boxes
Phase 5 — customtkinter · Session 3 of 8
What we’re learning today
Last week your buttons made things happen — but the user couldn’t type anything. Today we add input widgets — text fields the user types into. Combined with buttons, you’ll be able to build apps that take real input from the user and respond with personalized output. By the end of class, you’ll have a “form” app that asks for several pieces of info and uses them in a custom message.
You’ll need to remember from last time
CTkButtonwithcommand=function.- Callback functions.
label.configure(text=...)to update widgets.- Phase 3 strings — f-strings for combining text and variables.
Part A: Your first entry
Open Thonny. Save a new file as entry_app.py.
Build it
import customtkinter as ctk
def greet():
name = entry.get()
label.configure(text=f"Hello, {name}!")
app = ctk.CTk()
app.title("Greeter")
app.geometry("400x250")
entry = ctk.CTkEntry(app, placeholder_text="Type your name")
entry.pack(pady=20)
button = ctk.CTkButton(app, text="Greet me", command=greet)
button.pack(pady=10)
label = ctk.CTkLabel(app, text="", font=("Arial", 18))
label.pack(pady=20)
app.mainloop()
Save. Run.
A window opens with a text field, a button, and an empty label below. Type your name into the text field. Click the button. The label updates: “Hello, [your name]!”
What’s new
ctk.CTkEntry(app, placeholder_text="Type your name")— a single-line text input. Theplaceholder_textis the faded text that shows when empty.entry.get()— reads the current text in the entry. Returns a string.- The callback
greet()callsentry.get(), thenlabel.configure(text=...)to update the display.
The pattern: type into entry → click button → callback reads entry value → callback updates display. Same loop as the counter, just with text input instead of a click.
Customize and try variations
Try:
- An empty starting label (
text="") so nothing shows until the button is clicked. - A different greeting in the f-string (“Welcome, ___!” or “Hi there, ___!”).
- Make the entry bigger:
width=300. - Make the font of the entry bigger:
font=("Arial", 16).
Setting and clearing the entry
You can also set the entry’s text from code:
entry.delete(0, "end") # clear it
entry.insert(0, "default") # put text in
Useful when you want to clear after the user submits, or provide a default.
Try adding entry.delete(0, "end") at the end of greet():
def greet():
name = entry.get()
label.configure(text=f"Hello, {name}!")
entry.delete(0, "end") # clear after greeting
Now the entry empties out after each greeting. More form-like.
Checkpoint: You have a window with at least one entry, one button, and a label that updates with the entry’s content when the button is clicked. This is the natural stop point if class is cut short.
Part B: A multi-input form
Real apps usually have several fields. Build a small form that takes multiple pieces of info.
Base goal — about-me form
import customtkinter as ctk
def submit():
name = name_entry.get()
age = age_entry.get()
color = color_entry.get()
output_label.configure(
text=f"Hi {name}! You're {age} years old and your favorite color is {color}.",
wraplength=350
)
app = ctk.CTk()
app.title("About Me Form")
app.geometry("450x400")
title = ctk.CTkLabel(app, text="Tell me about you!", font=("Arial", 20, "bold"))
title.pack(pady=15)
name_entry = ctk.CTkEntry(app, placeholder_text="Name", width=300)
name_entry.pack(pady=5)
age_entry = ctk.CTkEntry(app, placeholder_text="Age", width=300)
age_entry.pack(pady=5)
color_entry = ctk.CTkEntry(app, placeholder_text="Favorite color", width=300)
color_entry.pack(pady=5)
submit_button = ctk.CTkButton(app, text="Submit", command=submit, width=200)
submit_button.pack(pady=15)
output_label = ctk.CTkLabel(app, text="", font=("Arial", 14))
output_label.pack(pady=20)
app.mainloop()
Save. Run. Fill in the three fields. Click submit. A personalized message appears.
What’s new:
- Multiple entries — each one is its own widget with its own variable name.
wraplength=350on the label — wraps the text after 350 pixels so long messages don’t run off the side.- The pattern scales — one callback that reads several entries and updates one label.
Stretch — a quiz that grades you
Ask a few questions, check the answers, give a score:
import customtkinter as ctk
def grade():
score = 0
if answer1.get().lower() == "noah":
score = score + 1
if answer2.get().lower() == "12":
score = score + 1
if answer3.get().lower().strip() == "moses":
score = score + 1
result_label.configure(text=f"Score: {score} / 3")
app = ctk.CTk()
app.title("Bible Quiz")
app.geometry("500x400")
title = ctk.CTkLabel(app, text="Bible Quiz", font=("Arial", 24, "bold"))
title.pack(pady=10)
q1 = ctk.CTkLabel(app, text="1. Who built the ark?")
q1.pack()
answer1 = ctk.CTkEntry(app, width=300)
answer1.pack(pady=5)
q2 = ctk.CTkLabel(app, text="2. How many disciples did Jesus have?")
q2.pack()
answer2 = ctk.CTkEntry(app, width=300)
answer2.pack(pady=5)
q3 = ctk.CTkLabel(app, text="3. Who led the Israelites out of Egypt?")
q3.pack()
answer3 = ctk.CTkEntry(app, width=300)
answer3.pack(pady=5)
button = ctk.CTkButton(app, text="Submit answers", command=grade)
button.pack(pady=10)
result_label = ctk.CTkLabel(app, text="", font=("Arial", 18))
result_label.pack(pady=15)
app.mainloop()
Run. Answer the questions. See your score.
The .lower() and .strip() calls handle different ways
the user might type the answer (capital, lowercase, with or
without trailing spaces).
Extension — a textbox for longer input
Sometimes you want the user to write paragraphs, not just
single lines. That’s a CTkTextbox:
import customtkinter as ctk
def count_words():
text = textbox.get("1.0", "end")
word_count = len(text.split())
label.configure(text=f"Word count: {word_count}")
app = ctk.CTk()
app.title("Word Counter")
app.geometry("500x400")
title = ctk.CTkLabel(app, text="Type something:", font=("Arial", 16))
title.pack(pady=10)
textbox = ctk.CTkTextbox(app, width=400, height=200)
textbox.pack(pady=10)
button = ctk.CTkButton(app, text="Count words", command=count_words)
button.pack(pady=5)
label = ctk.CTkLabel(app, text="", font=("Arial", 14))
label.pack(pady=10)
app.mainloop()
CTkTextbox is for multi-line text. To read its content,
use textbox.get("1.0", "end") — the "1.0" means “row 1,
column 0” (yes, rows count from 1, columns from 0; weird
but that’s tkinter). "end" means to the end.
The .split() on the result divides into words by
whitespace. len() counts them.
Wrap-up
Before we leave, share with the room:
- For the kids who built the form — what fields did you ask about?
- For the kids who built the quiz — what subject did you pick?
- Did anyone hit the “user typed something weird and it broke” issue? How did you handle it?
You learned today the foundation of forms — the interface element that powers every signup page, every search box, every comment field, every login. Plus buttons, plus output labels, plus updating widgets — that’s the entire shape of most productivity apps.
You also met the input pattern: widget.get() reads what
the user typed; the callback uses it. Same shape for
entries, textboxes, and (next week) checkboxes / radio
buttons / dropdowns.
If you missed this session
Open Thonny. Then:
-
Build the basic greeter from Part A — entry + button + label that updates on click.
-
Try
entry.delete(0, "end")to clear after greeting. -
Build the about-me form (Part B base) with three entries.
About 30-40 minutes. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
- Validation — check that input is a number before
using it.
if not age.isdigit(): .... Usetry/except ValueErrorfor converting strings to integers. bindto Enter key —entry.bind("<Return>", lambda e: submit())makes pressing Enter submit the form (instead of having to click the button).focus_set()—entry.focus_set()puts the cursor in the entry. Useful at startup or after submission.StringVar— a special variable type that automatically syncs with widgets. More advanced; powerful for two-way binding.
What’s next
Next week we add choice widgets — checkboxes, radio buttons, and dropdowns. Combined with today’s text inputs, you’ll have every common form element. After that, layouts to organize them all nicely.
Session 4: Choices — checkboxes, radio buttons, dropdowns
Session 4: Choices — checkboxes, radio buttons, dropdowns
Phase 5 — customtkinter · Session 4 of 8
What we’re learning today
You’ve got buttons and text input. Today we add the rest of the basic widget toolbox: checkboxes (yes/no multi-select), radio buttons (pick one of several), and dropdowns (pick from a list). By the end, you’ll have built a “build your character” form using all three.
You’ll need to remember from last time
CTkButtonwith callbacks.CTkEntryandentry.get().label.configure(text=...)to update widgets.- f-strings for combining text and variables.
Part A: Checkboxes, radios, dropdowns
Open Thonny. Save a new file as choices_app.py.
Checkboxes
A checkbox is yes/no. The user clicks to toggle. You can have many checkboxes, each with its own state.
import customtkinter as ctk
def show_choices():
likes = []
if pizza_check.get() == 1:
likes.append("pizza")
if soccer_check.get() == 1:
likes.append("soccer")
if reading_check.get() == 1:
likes.append("reading")
if len(likes) == 0:
result.configure(text="You like nothing? :(")
else:
result.configure(text=f"You like: {', '.join(likes)}")
app = ctk.CTk()
app.title("What do you like?")
app.geometry("400x350")
title = ctk.CTkLabel(app, text="Pick what you like:", font=("Arial", 16))
title.pack(pady=10)
pizza_check = ctk.CTkCheckBox(app, text="Pizza")
pizza_check.pack(pady=5)
soccer_check = ctk.CTkCheckBox(app, text="Soccer")
soccer_check.pack(pady=5)
reading_check = ctk.CTkCheckBox(app, text="Reading")
reading_check.pack(pady=5)
button = ctk.CTkButton(app, text="Show choices", command=show_choices)
button.pack(pady=10)
result = ctk.CTkLabel(app, text="", font=("Arial", 14), wraplength=350)
result.pack(pady=10)
app.mainloop()
Save. Run. Check some boxes. Click button. The label shows your choices.
What’s new:
CTkCheckBox(app, text="Label")— the checkbox widget.checkbox.get()returns1if checked,0if not.- The pattern: build up a list based on which boxes are checked.
Each checkbox is independent — they don’t affect each other. The user can check zero, one, two, or all of them.
Radio buttons
Radio buttons are different — only one can be selected at a time. Like a multiple-choice question. Click one and any previously selected one unchecks.
To make several radio buttons share state, you give them
all the same variable argument:
import customtkinter as ctk
def show_pick():
pick = pet_choice.get()
result.configure(text=f"You picked: {pick}")
app = ctk.CTk()
app.title("Pick a pet")
app.geometry("400x300")
title = ctk.CTkLabel(app, text="What kind of pet?", font=("Arial", 16))
title.pack(pady=10)
# all three radios share this variable
pet_choice = ctk.StringVar(value="dog") # default selection
dog_radio = ctk.CTkRadioButton(app, text="Dog", variable=pet_choice, value="dog")
dog_radio.pack(pady=5)
cat_radio = ctk.CTkRadioButton(app, text="Cat", variable=pet_choice, value="cat")
cat_radio.pack(pady=5)
fish_radio = ctk.CTkRadioButton(app, text="Fish", variable=pet_choice, value="fish")
fish_radio.pack(pady=5)
button = ctk.CTkButton(app, text="Submit", command=show_pick)
button.pack(pady=10)
result = ctk.CTkLabel(app, text="", font=("Arial", 14))
result.pack(pady=10)
app.mainloop()
Save. Run. Pick a pet. Click submit. The label shows your choice.
What’s new:
ctk.StringVar(value="dog")— a special variable for sharing state between widgets. We give it a starting value ("dog"is selected at startup).variable=pet_choiceon each radio — connects the radio to the shared variable.value="dog"(etc.) — what the variable becomes when this radio is selected.pet_choice.get()— reads the current value.
The StringVar is the new piece. Think of it as a “shared
text variable” that multiple widgets can reference. When one
radio is clicked, the variable updates to that radio’s
value. Reading the variable tells you which is selected.
Dropdowns
A dropdown shows one item but lets the user expand to pick from a list. Compact for many options.
import customtkinter as ctk
def show_color():
chosen = color_dropdown.get()
result.configure(text=f"Color: {chosen}", text_color=chosen)
app = ctk.CTk()
app.title("Pick a color")
app.geometry("400x250")
title = ctk.CTkLabel(app, text="Pick a color:", font=("Arial", 16))
title.pack(pady=10)
color_dropdown = ctk.CTkOptionMenu(
app,
values=["red", "blue", "green", "purple", "orange", "pink"]
)
color_dropdown.pack(pady=10)
button = ctk.CTkButton(app, text="Show", command=show_color)
button.pack(pady=10)
result = ctk.CTkLabel(app, text="", font=("Arial", 18))
result.pack(pady=10)
app.mainloop()
Save. Run. Click the dropdown. Pick a color. Click “Show.” The label updates with the color name AND in that color.
What’s new:
CTkOptionMenu(app, values=[...])— the dropdown widget. The list is what shows when you click it.dropdown.get()returns the currently-selected string.
There’s also CTkComboBox which is similar but lets the
user type a custom value (in addition to picking from
the list). Use that when the options aren’t fixed.
Checkpoint: You’ve used at least one of each: a checkbox, a radio button group, and a dropdown. This is the natural stop point if class is cut short.
Part B: A “build your character” form
Time to combine everything into one form.
Build it
import customtkinter as ctk
def build_character():
name = name_entry.get()
char_class = class_choice.get()
weapon = weapon_dropdown.get()
abilities = []
if magic_check.get() == 1:
abilities.append("magic")
if stealth_check.get() == 1:
abilities.append("stealth")
if strength_check.get() == 1:
abilities.append("strength")
abilities_text = ", ".join(abilities) if abilities else "none"
result.configure(
text=f"Meet {name} the {char_class}!\n"
f"Wields a {weapon}.\n"
f"Special abilities: {abilities_text}.",
wraplength=350
)
app = ctk.CTk()
app.title("Build a Character")
app.geometry("500x600")
title = ctk.CTkLabel(app, text="🗡️ Build Your Character", font=("Arial", 22, "bold"))
title.pack(pady=15)
# Name input
name_label = ctk.CTkLabel(app, text="Name:")
name_label.pack()
name_entry = ctk.CTkEntry(app, placeholder_text="Enter character name", width=300)
name_entry.pack(pady=5)
# Class (radio buttons)
class_label = ctk.CTkLabel(app, text="Class:")
class_label.pack(pady=(15, 0))
class_choice = ctk.StringVar(value="warrior")
warrior_radio = ctk.CTkRadioButton(app, text="Warrior", variable=class_choice, value="warrior")
warrior_radio.pack()
mage_radio = ctk.CTkRadioButton(app, text="Mage", variable=class_choice, value="mage")
mage_radio.pack()
rogue_radio = ctk.CTkRadioButton(app, text="Rogue", variable=class_choice, value="rogue")
rogue_radio.pack()
# Weapon (dropdown)
weapon_label = ctk.CTkLabel(app, text="Weapon:")
weapon_label.pack(pady=(15, 0))
weapon_dropdown = ctk.CTkOptionMenu(
app,
values=["sword", "staff", "dagger", "bow", "hammer"]
)
weapon_dropdown.pack(pady=5)
# Abilities (checkboxes)
abilities_label = ctk.CTkLabel(app, text="Special abilities:")
abilities_label.pack(pady=(15, 0))
magic_check = ctk.CTkCheckBox(app, text="Magic")
magic_check.pack()
stealth_check = ctk.CTkCheckBox(app, text="Stealth")
stealth_check.pack()
strength_check = ctk.CTkCheckBox(app, text="Strength")
strength_check.pack()
# Submit button
button = ctk.CTkButton(app, text="Build!", command=build_character)
button.pack(pady=15)
# Result
result = ctk.CTkLabel(app, text="", font=("Arial", 14), wraplength=350)
result.pack(pady=10)
app.mainloop()
Save. Run. Fill out the form — name, class, weapon, any abilities you want. Click Build. The label shows your character.
That’s the base goal — every choice widget type plus text input plus a button, all combined into one form.
Stretch — react in real-time
Make the result update as the user changes choices —
no submit button needed. Use command= on the choice
widgets:
def update():
name = name_entry.get() or "(no name)"
char_class = class_choice.get()
# ... build the result string ...
result.configure(text=...)
# attach update to each widget:
warrior_radio = ctk.CTkRadioButton(app, ..., command=update)
mage_radio = ctk.CTkRadioButton(app, ..., command=update)
weapon_dropdown = ctk.CTkOptionMenu(app, ..., command=lambda x: update())
magic_check = ctk.CTkCheckBox(app, ..., command=update)
# ... etc.
(For OptionMenu, the command takes one argument — the
selected value — so we use lambda x: update() to ignore
it.)
For text entries, you’d bind to the keypress event:
name_entry.bind("<KeyRelease>", lambda e: update())
Now the character description updates in real time as the user changes anything. That’s how modern reactive apps feel.
Extension — save the character
Add a “Save” button that writes the character to a file (callback to Phase 3 Session 11):
def save():
name = name_entry.get()
char_class = class_choice.get()
weapon = weapon_dropdown.get()
with open("characters.txt", "a") as f:
f.write(f"{name}, {char_class}, {weapon}\n")
result.configure(text=f"Saved {name} to characters.txt!")
# add another button:
save_button = ctk.CTkButton(app, text="Save", command=save)
save_button.pack(pady=5)
Now characters persist between runs of the app. A real mini-app.
Wrap-up
Before we leave, share with the room:
- For the kids who built the character form — what’s your best character?
- For the kids who tried real-time updates — does it feel more “alive” than clicking submit?
- Did anyone get the radio buttons to work without a StringVar? (Trick question — they need it.)
You learned today the complete form vocabulary — text input, choice widgets, buttons. Combined with last week’s labels and update patterns, you can now build any form an app might need. Login pages, settings, surveys, configuration — all the same shapes.
You also met StringVar (and the related IntVar,
DoubleVar, BooleanVar) — special variables that let
multiple widgets share state. Required for radio buttons;
useful for many other things.
If you missed this session
Open Thonny. Then:
-
Build the basic checkbox example from Part A. Check some boxes. Click submit. See the result.
-
Build the radio button example. Notice how only one can be selected.
-
Build the dropdown example.
-
Build the character form (Part B base) using all three widget types plus an entry.
About 40-50 minutes — this is a long session.
Stretch and extension ideas
SegmentedButton— like radio buttons but as a row of buttons.ctk.CTkSegmentedButton(app, values=[...]).Slider— for numeric input.ctk.CTkSlider(app, from_=0, to=100).Switch— like a checkbox but styled as a toggle switch.ctk.CTkSwitch(app, text="...").ProgressBar— for showing progress.- Disable widgets based on choices — e.g., the magic
checkbox is only available if class is “Mage.” Use
widget.configure(state="disabled")andstate="normal".
What’s next
Next week we tackle layouts — how to organize all these
widgets so they look good together. So far we’ve used
pack() which just stacks vertically. Next week we’ll use
grid() and CTkFrame to make multi-column, multi-section
layouts that look like real apps.
Session 5: Layouts and frames
Session 5: Layouts and frames
Phase 5 — customtkinter · Session 5 of 8
What we’re learning today
Your forms are getting long. Every pack() adds another
widget below the last — fine for a few widgets, awkward for
a real app. Today we learn grid() for arranging
widgets in rows and columns, and CTkFrame for grouping
related widgets together. By the end, you’ll have a
multi-section app that looks like a real app, not just
a stack of widgets.
You’ll need to remember from last time
- Widgets — labels, buttons, entries, checkboxes, radios, dropdowns.
pack(pady=10)— the layout method we’ve been using.- f-strings for combining text.
Part A: grid() — rows and columns
Open Thonny. Save a new file as grid_demo.py.
The motivation
pack() is great for one-widget-per-row. But what if you
want two columns — like a label next to an entry?
Today’s first try:
import customtkinter as ctk
app = ctk.CTk()
app.title("Grid demo")
app.geometry("400x250")
# label and entry on the same "row"
name_label = ctk.CTkLabel(app, text="Name:")
name_label.grid(row=0, column=0, padx=10, pady=10)
name_entry = ctk.CTkEntry(app)
name_entry.grid(row=0, column=1, padx=10, pady=10)
age_label = ctk.CTkLabel(app, text="Age:")
age_label.grid(row=1, column=0, padx=10, pady=10)
age_entry = ctk.CTkEntry(app)
age_entry.grid(row=1, column=1, padx=10, pady=10)
button = ctk.CTkButton(app, text="Submit")
button.grid(row=2, column=0, columnspan=2, pady=15)
app.mainloop()
Save. Run.
Two rows of label-and-entry pairs, perfectly aligned in two columns. Then a button below that spans both columns.
What’s new
widget.grid(row=R, column=C, ...)— places the widget at row R, column C. Rows count from 0 (top); columns from 0 (left).padx=10, pady=10— padding around each widget (same as pack).columnspan=2— makes the widget span 2 columns. (rowspan=2does the equivalent for rows.)
grid() and pack() don’t mix in the same parent
container. Pick one per container and stick with it.
Important rule: in any window or frame, use either
pack()orgrid()— not both. Mixing them breaks the layout in confusing ways. (You can use different ones in different frames; we’ll see frames next.)
When you mix them anyway (the bug you’ll write)
This is the one mistake every Python GUI beginner makes.
You start a project with pack(), switch over to grid()
mid-file, and end up with something like:
# DON'T do this — mixed pack and grid in the same window
name_label = ctk.CTkLabel(app, text="Name:")
name_label.pack(pady=5) # ← pack
name_entry = ctk.CTkEntry(app)
name_entry.grid(row=1, column=1) # ← grid
Run that. The window hangs. It looks like Python is
frozen — it’s not. Behind the scenes, customtkinter’s layout
engine is in an infinite back-and-forth: pack resizes the
window, grid resizes it differently, repeat forever. You
hit the red Stop button to escape.
Fix: pick one geometry manager per container. To use
grid for the form, change every pack(...) in that
window to a grid(row=..., column=...):
name_label = ctk.CTkLabel(app, text="Name:")
name_label.grid(row=0, column=0, padx=5, pady=5) # ← grid
name_entry = ctk.CTkEntry(app)
name_entry.grid(row=1, column=1, padx=5, pady=5) # ← grid
If your window ever hangs as soon as it opens, this is the first thing to check.
A bigger grid
Try a 3-column layout:
import customtkinter as ctk
app = ctk.CTk()
app.title("3-column grid")
app.geometry("500x300")
# header row spans all 3 columns
header = ctk.CTkLabel(app, text="My App", font=("Arial", 24, "bold"))
header.grid(row=0, column=0, columnspan=3, pady=10)
# row of labels
ctk.CTkLabel(app, text="Name").grid(row=1, column=0, padx=10)
ctk.CTkLabel(app, text="Age").grid(row=1, column=1, padx=10)
ctk.CTkLabel(app, text="Color").grid(row=1, column=2, padx=10)
# row of entries
ctk.CTkEntry(app).grid(row=2, column=0, padx=10, pady=5)
ctk.CTkEntry(app).grid(row=2, column=1, padx=10, pady=5)
ctk.CTkEntry(app).grid(row=2, column=2, padx=10, pady=5)
# submit button across the bottom
button = ctk.CTkButton(app, text="Submit")
button.grid(row=3, column=0, columnspan=3, pady=15)
app.mainloop()
Run. Three columns of input. Way more compact than single-column pack.
Notice the inline pattern: ctk.CTkLabel(app, text="..."). grid(...). We didn’t save the widget in a variable because
we don’t need to read or modify it later. Saves typing.
sticky — alignment within a cell
Each grid cell has a fixed size. Inside it, a widget can
align to any side. sticky="w" (west = left), "e" (east),
"n" (north), "s" (south), or combinations like "ew"
(stretches across).
ctk.CTkLabel(app, text="Right-aligned:").grid(row=0, column=0, sticky="e", padx=5)
Useful for aligning labels to the right of their column so they sit next to the entries on the left of the next.
Checkpoint: You’ve built a layout with grid() that
has at least two columns and at least two rows. This is
the natural stop point if class is cut short.
Part B: Frames — grouping widgets
When forms get bigger, they need sections — visual
groupings of related widgets. That’s a CTkFrame.
What a frame is
A CTkFrame is a container widget. It holds other
widgets inside it. You can pack/grid widgets inside a
frame, then pack/grid the frame inside the main window.
Frames give you:
- Visual grouping (you can color the background).
- Layout flexibility (one frame can use grid, another can use pack).
- Easier reorganization (move a frame and everything in it moves together).
A two-section app
import customtkinter as ctk
app = ctk.CTk()
app.title("Two-section app")
app.geometry("600x400")
# Left frame — input
left_frame = ctk.CTkFrame(app)
left_frame.pack(side="left", fill="both", expand=True, padx=10, pady=10)
ctk.CTkLabel(left_frame, text="Input", font=("Arial", 18, "bold")).pack(pady=10)
ctk.CTkEntry(left_frame, placeholder_text="Name").pack(pady=5)
ctk.CTkEntry(left_frame, placeholder_text="Age").pack(pady=5)
ctk.CTkButton(left_frame, text="Submit").pack(pady=10)
# Right frame — output
right_frame = ctk.CTkFrame(app)
right_frame.pack(side="right", fill="both", expand=True, padx=10, pady=10)
ctk.CTkLabel(right_frame, text="Output", font=("Arial", 18, "bold")).pack(pady=10)
ctk.CTkLabel(right_frame, text="(submit something to see results)").pack(pady=5)
app.mainloop()
Save. Run.
Two frames side by side. Left holds inputs; right holds
output. Inside each frame, widgets stack with pack().
What’s new:
CTkFrame(app)— a frame that lives in the main window.pack(side="left", fill="both", expand=True)— packs the frame to the left side; fills available space; expands as the window grows.- Widgets inside the frame are children of the frame, not
of the app. So
ctk.CTkLabel(left_frame, ...)puts the label insideleft_frame, not the main window.
The side="left" and side="right" is pack()’s way of
doing horizontal layout (instead of the default vertical).
You can also use "top" and "bottom".
Inside a frame, use
pack()orgrid()independently of the parent. The “no mixing” rule applies within each container, not across the whole app.
A real layout — header, sidebar, content
import customtkinter as ctk
app = ctk.CTk()
app.title("App with sections")
app.geometry("700x500")
# Header (top)
header = ctk.CTkFrame(app, height=60)
header.pack(side="top", fill="x", padx=5, pady=5)
ctk.CTkLabel(header, text="My App", font=("Arial", 24, "bold")).pack(pady=10)
# Body — sidebar + content
body = ctk.CTkFrame(app)
body.pack(side="top", fill="both", expand=True, padx=5, pady=5)
# Sidebar (left part of body)
sidebar = ctk.CTkFrame(body, width=150)
sidebar.pack(side="left", fill="y", padx=5, pady=5)
ctk.CTkLabel(sidebar, text="Menu", font=("Arial", 14, "bold")).pack(pady=10)
ctk.CTkButton(sidebar, text="Home").pack(pady=5)
ctk.CTkButton(sidebar, text="Settings").pack(pady=5)
ctk.CTkButton(sidebar, text="About").pack(pady=5)
# Content (right part of body)
content = ctk.CTkFrame(body)
content.pack(side="right", fill="both", expand=True, padx=5, pady=5)
ctk.CTkLabel(content, text="Welcome!", font=("Arial", 32)).pack(pady=30)
ctk.CTkLabel(content, text="This is a real-looking app layout.").pack(pady=5)
app.mainloop()
Save. Run.
A header bar across the top. A sidebar on the left with buttons. A main content area on the right. This actually looks like a real app.
Frames within frames. Each one organizes its part of the window.
That’s the base goal — a multi-frame layout that looks like a real app.
Stretch — make the sidebar do something
Add callbacks to the sidebar buttons so they update the content area:
def show_home():
content_label.configure(text="🏠 Home")
def show_settings():
content_label.configure(text="⚙️ Settings")
def show_about():
content_label.configure(text="ℹ️ About this app")
# ...
ctk.CTkButton(sidebar, text="Home", command=show_home).pack(pady=5)
ctk.CTkButton(sidebar, text="Settings", command=show_settings).pack(pady=5)
ctk.CTkButton(sidebar, text="About", command=show_about).pack(pady=5)
# in the content frame:
content_label = ctk.CTkLabel(content, text="🏠 Home", font=("Arial", 32))
content_label.pack(pady=30)
Now clicking a sidebar button changes the content. That’s how every navigation-based app works.
Extension — wrap the whole thing in a class
If you’ve got time, refactor the whole app into a class (callback to Phase 4 Session 4):
import customtkinter as ctk
class MyApp:
def __init__(self):
self.app = ctk.CTk()
self.app.title("My App")
self.app.geometry("700x500")
self._build_header()
self._build_sidebar_and_content()
def _build_header(self):
header = ctk.CTkFrame(self.app, height=60)
header.pack(side="top", fill="x", padx=5, pady=5)
ctk.CTkLabel(header, text="My App", font=("Arial", 24, "bold")).pack(pady=10)
def _build_sidebar_and_content(self):
body = ctk.CTkFrame(self.app)
body.pack(side="top", fill="both", expand=True, padx=5, pady=5)
sidebar = ctk.CTkFrame(body, width=150)
sidebar.pack(side="left", fill="y", padx=5, pady=5)
ctk.CTkButton(sidebar, text="Home", command=self.show_home).pack(pady=5)
ctk.CTkButton(sidebar, text="Settings", command=self.show_settings).pack(pady=5)
content = ctk.CTkFrame(body)
content.pack(side="right", fill="both", expand=True, padx=5, pady=5)
self.content_label = ctk.CTkLabel(content, text="🏠 Home", font=("Arial", 32))
self.content_label.pack(pady=30)
def show_home(self):
self.content_label.configure(text="🏠 Home")
def show_settings(self):
self.content_label.configure(text="⚙️ Settings")
def run(self):
self.app.mainloop()
app = MyApp()
app.run()
Same app, organized as a class. State (self.content_label)
lives on the instance. Methods access it via self. No
global needed. This is how real GUI apps are organized.
Wrap-up
Before we leave, share with the room:
- For the kids who built the two-section app — does side-by-side feel cleaner than stacked?
- For the kids who built the header-sidebar-content layout — does it feel like a real app?
- For the class refactor — does the class version feel more organized, or just longer?
You learned today the organizational toolkit for GUI
apps. grid() for precise rows-and-columns layouts.
CTkFrame for grouping related widgets. Combined,
they let you build interfaces that look like real software
— not just stacked widgets.
Most real apps are built as frames within frames — a header frame, a body frame, a sidebar frame, a content frame. Each one self-contained. Each one easy to rearrange. That’s the structural pattern of every GUI app you’ve ever used.
If you missed this session
Open Thonny. Then:
-
Build the basic 2-column form from Part A using
grid()instead ofpack(). -
Try
columnspanto make a button span multiple columns. -
Build the two-section app with frames from Part B.
-
Build the header-sidebar-content layout (Part B base).
About 50 minutes — this is a long session. If you get stuck, ask your buddy at the start of next class.
Stretch and extension ideas
grid_rowconfigure(weight=1)— makes a row grow when the window resizes. Useful for “expand this row to fill space.”grid_columnconfigure(weight=1)— same for columns.place()— a third layout method that puts widgets at exact pixel positions. Almost never used in practice (windows resize and pixel positions break). Mention as “exists; usually don’t.”- Border styles on frames —
CTkFrame(app, border_width=2, border_color="gray")for visible borders. - Scrollable frames —
CTkScrollableFramefor content that’s bigger than the window.
What’s next
Next week we put it all together — building a complete app using everything from Phase 5 so far. After that, you have one milestone planning session and one demo session, then Phase 5 is done.
Session 6: Putting it together — a complete app
Session 6: Putting it together — a complete app
Phase 5 — customtkinter · Session 6 of 8
What we’re learning today
You’ve met every basic widget: labels, buttons, entries,
checkboxes, radios, dropdowns, frames. You’ve learned pack
and grid. Today we put it all together and build a
complete, real app — a todo list. Add tasks, see them
in a scrollable list, delete them, and (stretch) save them
to a file so they survive between runs.
By the end you’ll have something that looks and feels like real software — the kind of thing you could actually use.
You’ll need to remember from last time
grid()andCTkFrame— Session 5.CTkEntryandentry.get()— Session 3.CTkButtonwith callbacks — Session 2.label.configure(text=...)— Session 2.- Lists in Python — Phase 3 Session 8.
- Reading and writing files — Phase 3 Session 11.
- Light classes — Phase 4 Session 4.
Part A: Build the todo list
Open Thonny. Save a new file as todos.py.
We’ll build this in steps. Run after each step to make sure each piece works before moving on.
Step 1 — The window and the title
import customtkinter as ctk
app = ctk.CTk()
app.title("My Todos")
app.geometry("400x500")
title = ctk.CTkLabel(app, text="My Todos", font=("Arial", 24, "bold"))
title.pack(pady=10)
app.mainloop()
Save. Run. A window with a title. So far, nothing new.
Step 2 — The input row
We want an entry on the left and an “Add” button on the right, sitting in a row across the top. That’s a frame with two widgets in it, packed left and right.
Add this before app.mainloop():
input_frame = ctk.CTkFrame(app)
input_frame.pack(fill="x", padx=10, pady=10)
entry = ctk.CTkEntry(input_frame, placeholder_text="What needs doing?")
entry.pack(side="left", fill="x", expand=True, padx=5, pady=5)
add_button = ctk.CTkButton(input_frame, text="Add", width=80)
add_button.pack(side="right", padx=5, pady=5)
Save. Run. The entry stretches across; the Add button sits on the right. Click “Add” — nothing happens yet (no callback). That’s next.
Step 3 — The list area
Below the input row, we want a scrollable area where the
todos will appear. customtkinter has a built-in widget for
this: CTkScrollableFrame. Widgets inside it scroll
automatically once they overflow.
Add this below the input frame setup:
list_frame = ctk.CTkScrollableFrame(app)
list_frame.pack(fill="both", expand=True, padx=10, pady=10)
Save. Run. The window now has a scrollable area in the middle. Empty for now. We’ll fill it with todo rows.
Step 4 — Storing and showing todos
Now the meat. We need:
- A list to store the todos.
- A function to add a todo (called when Add is clicked).
- A function to redraw the list area whenever it changes.
Add this near the top of your file (right after the
import):
todos = []
def add_todo():
text = entry.get().strip()
if not text:
return
todos.append(text)
entry.delete(0, "end")
refresh_list()
def refresh_list():
# First, clear out the existing rows
for widget in list_frame.winfo_children():
widget.destroy()
# Then re-create one row per todo
for todo in todos:
row = ctk.CTkFrame(list_frame)
row.pack(fill="x", pady=2)
label = ctk.CTkLabel(row, text=todo, anchor="w")
label.pack(side="left", fill="x", expand=True, padx=5)
Then connect the Add button to add_todo. Change the
add_button line to:
add_button = ctk.CTkButton(input_frame, text="Add", command=add_todo, width=80)
Save. Run. Type something. Click Add. Your todo appears in the list. Type another. Click Add. Both appear.
There’s a lot going on here — let’s break it down:
text = entry.get().strip()reads what the user typed and removes leading/trailing spaces.if not text: returnignores empty entries (clicking Add with nothing typed does nothing).todos.append(text)adds to the list.entry.delete(0, "end")clears the entry box.refresh_list()redraws the list area from scratch.
The “redraw from scratch” pattern is useful: instead of trying to add or remove just the right widget, we wipe everything and rebuild. Simpler to think about, and performance is fine for small lists.
winfo_children() gives us all widgets inside a parent
— here, all the rows we previously created in list_frame.
We loop through and destroy each one before drawing the new
list.
Step 5 — Delete buttons
Each row should have a delete button (an “×”) on the right. Click it, that todo is gone.
This is trickier than it looks because each row needs its
own delete callback that knows which todo to delete.
We’ll use a trick: a lambda that captures the index.
Add a delete function:
def delete_todo(index):
todos.pop(index)
refresh_list()
Then update refresh_list to also create a delete button on
each row. Replace the inner for todo in todos: loop with:
for i, todo in enumerate(todos):
row = ctk.CTkFrame(list_frame)
row.pack(fill="x", pady=2)
label = ctk.CTkLabel(row, text=todo, anchor="w")
label.pack(side="left", fill="x", expand=True, padx=5)
delete_button = ctk.CTkButton(
row,
text="×",
width=30,
command=lambda idx=i: delete_todo(idx)
)
delete_button.pack(side="right", padx=5)
Save. Run. Add a few todos. Click the “×” next to one. It disappears. Add more. Delete others. The list updates each time.
What’s new:
enumerate(todos)gives(index, item)pairs as you loop. We need both — the index for deletion, the item for the label.lambda idx=i: delete_todo(idx)is the tricky part. We capture the current value ofiasidx, then calldelete_todo(idx)when the button is clicked. Without theidx=ipart, all buttons would end up callingdelete_todowith the last value ofi. (This is a classic Python gotcha. Don’t dwell on it — just remember the pattern.)
Checkpoint: You have a todo app — add, view, delete. This is the natural stop point if class is cut short.
Part B: Save to a file (and a class refactor)
The app works great — until you close it. Then everything disappears. Real apps persist state between runs. Let’s fix that.
Save and load to JSON
We’ll use the json module from Phase 4 Session 3. JSON is
the standard format for saving structured data like lists.
Add to the top of your file:
import json
import os
SAVE_FILE = "todos.json"
Add two functions:
def save():
with open(SAVE_FILE, "w") as f:
json.dump(todos, f)
def load():
if os.path.exists(SAVE_FILE):
with open(SAVE_FILE, "r") as f:
return json.load(f)
return []
Now call them from the right places. At the top, change
todos = [] to:
todos = load()
In add_todo and delete_todo, add save() after the
list change:
def add_todo():
text = entry.get().strip()
if not text:
return
todos.append(text)
entry.delete(0, "end")
save()
refresh_list()
def delete_todo(index):
todos.pop(index)
save()
refresh_list()
And finally, call refresh_list() once before
app.mainloop() so any todos from a previous run show
up immediately:
refresh_list()
app.mainloop()
Save. Run. Add some todos. Close the app. Open a file
explorer — you should see todos.json next to your script.
Run the app again. Your todos are back!
This is what makes an app feel real. State that survives the program closing.
Stretch — class refactor
Phase 4 Session 4 introduced classes. Phase 5 Session 5 showed a class refactor. This is the same pattern, applied to your todo app.
A new file, todos_class.py:
import customtkinter as ctk
import json
import os
SAVE_FILE = "todos.json"
class TodoApp:
def __init__(self):
self.todos = self._load()
self.app = ctk.CTk()
self.app.title("My Todos")
self.app.geometry("400x500")
self._build_ui()
self._refresh_list()
def _build_ui(self):
title = ctk.CTkLabel(self.app, text="My Todos", font=("Arial", 24, "bold"))
title.pack(pady=10)
input_frame = ctk.CTkFrame(self.app)
input_frame.pack(fill="x", padx=10, pady=10)
self.entry = ctk.CTkEntry(input_frame, placeholder_text="What needs doing?")
self.entry.pack(side="left", fill="x", expand=True, padx=5, pady=5)
self.entry.bind("<Return>", lambda e: self.add_todo())
add_button = ctk.CTkButton(input_frame, text="Add", command=self.add_todo, width=80)
add_button.pack(side="right", padx=5, pady=5)
self.list_frame = ctk.CTkScrollableFrame(self.app)
self.list_frame.pack(fill="both", expand=True, padx=10, pady=10)
def add_todo(self):
text = self.entry.get().strip()
if not text:
return
self.todos.append(text)
self.entry.delete(0, "end")
self._save()
self._refresh_list()
def delete_todo(self, index):
self.todos.pop(index)
self._save()
self._refresh_list()
def _refresh_list(self):
for widget in self.list_frame.winfo_children():
widget.destroy()
for i, todo in enumerate(self.todos):
row = ctk.CTkFrame(self.list_frame)
row.pack(fill="x", pady=2)
label = ctk.CTkLabel(row, text=todo, anchor="w")
label.pack(side="left", fill="x", expand=True, padx=5)
delete_button = ctk.CTkButton(
row, text="×", width=30,
command=lambda idx=i: self.delete_todo(idx)
)
delete_button.pack(side="right", padx=5)
def _save(self):
with open(SAVE_FILE, "w") as f:
json.dump(self.todos, f)
def _load(self):
if os.path.exists(SAVE_FILE):
with open(SAVE_FILE, "r") as f:
return json.load(f)
return []
def run(self):
self.app.mainloop()
if __name__ == "__main__":
TodoApp().run()
Save. Run. Same app — same behavior. But the code is organized differently:
- All state (
self.todos,self.entry,self.list_frame) lives on the instance. Noglobal. No magic top-level variables. - All behavior is methods. Related code sits next to related code.
- Underscore prefix (
_build_ui,_save,_load,_refresh_list) signals “internal helper, don’t call from outside the class.” A convention, not enforced. - The
if __name__ == "__main__"block creates the app and runs it. If you imported this file from another module, the app would not automatically start.
Also new: self.entry.bind("<Return>", lambda e: self.add_todo())
— pressing Enter in the entry box adds the todo without
clicking the button. Real apps do this. Try it.
This structure is what real production GUI code looks like. You’re writing real code now.
Extension — mark complete
Add a checkbox to each row. When checked, the todo is “done” — show it greyed out or with different text.
Hint: store todos as {"text": "...", "done": False}
dictionaries instead of plain strings. Then in
_refresh_list, render the label differently when
todo["done"] is True (try text_color="gray").
Wrap-up
Before we leave, share with the room:
- For the kids who finished Part A — show your todo list. How many todos did you add to test it?
- For the kids who got persistence working — close the app and reopen it. Magical, right?
- For the kids who refactored to a class — does the code feel more organized?
Today you built a real, useful app. Not a toy. The kind of thing you could actually run on your own machine and use to track real tasks. That’s the goal of Phase 5 — and you’re there.
You used:
- Layouts — frame inside a frame, side-packing.
- Widgets — labels, entries, buttons.
- State — a list that the UI reflects.
- Callbacks — functions that run on user action.
- The redraw pattern — wipe and rebuild on every change.
- Persistence — save to JSON, load on startup.
- Classes (stretch) — production-style organization.
Next two weeks are your project — your design, your code. You’re ready.
If you missed this session
Open Thonny. Then:
-
Build the todo list step by step from Part A. Don’t skip steps — each one builds on the last.
-
Test with a few todos. Add some, delete some.
-
(Stretch) Add the JSON save/load from Part B. Verify that closing and reopening keeps your list.
-
(Stretch) Read through the class refactor. Try running it. Notice how the code is organized.
About 60-90 minutes total — this is a long, substantial session.
Stretch and extension ideas
- Mark complete (extension above) — checkbox per row, greyed-out when done.
- Edit a todo — click on the label to make it editable.
Use a
CTkEntrythat swaps in for the label. - Sort or filter — show only undone todos; sort alphabetically; sort by date added.
- Multiple lists — a “shopping” list and a “homework”
list, switchable via tabs (
CTkTabview). - Due dates — store a date with each todo, color old ones red.
- Categories or tags — group todos by category.
- A header bar with “Clear completed” or “Clear all” buttons.
- Better styling — different colors, better fonts, icons in the buttons.
Each of these is a small project. Pick one or two for fun. Or save them as ideas for your milestone project.
What’s next
Next week is milestone planning + work day 1. You’ll design your desktop app — your idea, your design. You’ll spend the rest of the session building the simplest version. Week after that, you’ll finish, polish, and demo to the class.
Bring an idea or two next week. (Or come empty-handed — we have a seed list ready.)
Session 7: Milestone project work day 1
Session 7: Milestone project work day 1
Phase 5 — customtkinter · Session 7 of 8
What we’re learning today
Today is your day. You’ll plan a desktop app of your own design — your idea, your widgets, your behavior — and start building it. Next week you’ll finish it and demo it to the class.
You’ll need to remember from last time
- The widget toolbox — labels, buttons, entries, checkboxes, radios, dropdowns, frames.
packandgridfor layout.- Callbacks with
command=. - The redraw pattern — state + a refresh function.
- Persistence with JSON (Session 6 stretch).
- Git (Phase 4 Sessions 5-6) — commit as you go.
Part A: Plan your app
Same shape as previous milestones, with new requirements specific to Phase 5.
The plan
Take a piece of paper or open a blank text file. Answer these six questions:
-
What’s the app? (One sentence. “A flashcard quiz.” “A budget tracker.” “A drawing pad.”)
-
What does the user do with it? (How do they interact? What do they click? What do they type?)
-
What widgets will it have? (Labels, entries, buttons, lists, checkboxes? Sketch the window on paper if it helps.)
-
What state does it track? (A list of items? A single value? A dictionary?)
-
What’s the simplest version? (Build this FIRST. No persistence, no fancy features — just the core.)
-
Will it save data to a file? (Optional but recommended. JSON for lists/dicts, plain text for single values.)
If you don’t have an idea
Pick one and modify:
- A flashcard app — load questions from a file, show one at a time, click to reveal the answer, next button.
- A budget tracker — add expenses with category and amount, show running totals, save to JSON.
- A drawing pad —
CTkCanvas(similar to Phase 2 turtle world), click and drag to draw lines. - A timer / pomodoro app — set a duration, click start, watch the countdown.
- A dice roller for tabletop games — pick number of dice, click roll, show results.
- A unit converter — pick from/to units, type a value, see the result.
- A simple text editor —
CTkTextbox, save/load buttons, file path entry. - A character sheet for a game — name, stats, items (extension of Session 4’s character builder, with save).
- A quiz game — multiple choice questions, score tracking, end screen.
- A music player UI — list of songs, play/pause/skip buttons. (The buttons can fake it — print to console instead of actually playing audio.)
- A memory match game — grid of buttons that flip to reveal images or letters; match pairs.
Pick one. Spend two minutes. Don’t overthink.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question.
Set up the project
Create a folder for your project. Open a terminal:
$ cd ~ # or wherever you keep projects
$ mkdir my_app # use your project's name
$ cd my_app
$ git init
Create your main file (probably main.py or app.py) and
commit it before writing much:
$ git add main.py
$ git commit -m "Initial project setup"
Commit early, commit often. Each meaningful change gets its own commit.
Build the simplest version first
Look at your answer to question 5. Build that first. Get a window on screen. Get the most important widget working. Wire up one callback. Then iterate.
For each significant change:
- Make the change.
- Run it. Make sure it works.
- Commit (
git add ...; git commit -m "..."). - Move to the next thing.
If your widgets are getting unwieldy and there are lots of top-level globals, wrap your code in a class (like Session 6’s class refactor). Real apps tend to grow into classes.
If something feels risky (a big change you might want to undo), make a branch:
$ git checkout -b try-new-thing
# ... experiment ...
$ git checkout main
$ git merge try-new-thing # if it worked
# OR
$ git branch -D try-new-thing # if it didn't
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today. Bonus points for showing a screenshot.
Bring your project (the folder — Git carries the history) next week. We’ll finish, then demo.
If you missed this session
Open Thonny and a terminal. Then:
-
Spend 10-15 minutes answering the six planning questions.
-
Create a folder for your project.
git initin it. Create your first file. Commit. -
Build the simplest version. Commit as you go (every few meaningful changes).
About 60-75 minutes total — planning + a substantial first build. If you can show your buddy a window with at least one working button next week, you’re ready for Day 2.
If you don’t have an idea, the seed list above is a starting point. Pick one and modify.
Stretch and extension ideas
If your base project is working and you want to add more:
- Add JSON persistence so state survives between runs (Session 6 pattern).
- Refactor to a class if you started with globals (Session 6 stretch).
- Add a Settings dialog with
CTkToplevel— a separate window for app preferences. - Style it — try
ctk.set_appearance_mode("dark")andctk.set_default_color_theme("green")near the top. - Use a branch to try a feature without breaking main. Merge if it works.
- Add a menu bar with File / Edit / View options
(advanced —
tkinter.Menu). - Make it resizable nicely — use
gridweights so things scale with the window. - Add icons — buttons can have images via
image=.
Whatever you add, commit each change. Future you wants to see the journey.
What’s next
Next week is the last session of Phase 5. You’ll have time to finish, polish, and demo your app to the class. Each person gets 3-5 minutes. Bring a working app, your enthusiasm, and your Git log — part of the demo will be showing the journey of how your app came together.
Session 8: Milestone project work day 2 + demo day
Session 8: Milestone project work day 2 + demo day
Phase 5 — customtkinter · Session 8 of 8
What we’re learning today
Today is the last session of Phase 5. First half: finish your app — fix bugs, add the last features, make sure it runs cleanly. Second half: demo to the class. By the end, you’ll have shipped your fifth milestone project — and your first real desktop app.
You’ll need to remember from last time
- Your project plan from last week.
- Whatever you got working last week.
- Your buddy for testing.
- Git — commit your final changes today.
- All your widget tools — labels, buttons, entries, layouts, callbacks, state, persistence.
Part A: Final polish
You have about 35 minutes to finish.
What “finished” means
Same three rules as always:
- It runs without crashing under normal use.
- It does what your plan said it would do — at least the simplest version.
- You can explain how it works.
If your app doesn’t meet all three, focus on getting there before adding anything new.
Polish ideas (Phase 5 specific)
- Try it like a user. Click around. Type weird things. See what breaks. Fix the obvious bugs.
- Window size. Does it look right when you open it?
Adjust
app.geometry("WxH")to fit your widgets. - Window title. Make sure
app.title("...")says something meaningful. - Empty-state handling. If your app shows a list, what happens when the list is empty? An empty area is fine but a placeholder label is friendlier.
- Labels make sense. Buttons say what they do. Placeholder text in entries is helpful.
- Theme — try
ctk.set_appearance_mode("dark")andctk.set_default_color_theme("blue")(or “green”, “dark-blue”) near the top of your file. Looks polished. - Save your work — if your app stores data, make sure the save/load actually works. Close and reopen to test.
- Add a README —
README.mdin your project folder with one paragraph about what your app does and how to run it. Real projects have one. - Clean up commits. Look at
git log. Are messages clear? They don’t have to be perfect, but should describe what changed. - All committed.
git statusshould say “working tree clean” before the demo.
Buddy test
About 15 min in, swap with your buddy. They use your app for a few minutes; you use theirs. Notice things they get wrong (clicking the wrong button, not seeing what to type). Those are real UX bugs.
Checkpoint: Your app runs the basic version of what
you planned, and your git log shows the journey. This
is the natural stop point if class is cut short — but
today, demo time is next.
Part B: Demo day
Each person gets 3-5 minutes. Same format as before, plus the Phase 4 twist: show your Git log.
How a demo works
When it’s your turn:
- Show your app. Run it. Use it. Walk us through what it does.
- Tell us about it. What is it? Who is it for?
- Show your Git log. Run
git log --oneline. Walk us through how the app came together — first commit, last commit, a turning point. - Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected. Maybe also one thing that was easier than you thought.
- Take one question.
For Phase 5 demos especially, let the audience use the app if there’s time. A GUI is meant to be touched. Hand the keyboard or mouse to a buddy.
After everyone demos
Mr. Eric will say a few specific things about each project. Then we’re done — Phase 5 is complete.
What you accomplished
- You shipped your fifth milestone project.
- You used the customtkinter widget toolbox to build a real desktop app.
- You used Git to track every step of the journey.
- (If you used persistence) Your app has memory — state survives between runs.
- (If you used a class) You wrote production-style GUI code.
- You built something with a real interface — buttons, text inputs, layouts. Not a script, not a turtle drawing — a desktop app.
- You finished Phase 5. GUI programming is yours.
What’s next: Phase 6 — Pygame (games)
Phase 6 is games. Real, animated, interactive games with graphics, sounds, scoring, and game loops.
What’s different:
- Animation. Your customtkinter apps sat there waiting for the user. Pygame programs run a frame loop — 60 times a second, draw the screen and update.
- Real graphics. Sprites, images, colors, shapes.
- Sound. Play sound effects and background music.
- Real game design — collision detection, scoring, game over screens, levels.
- Bigger projects. Phase 6 is longer than Phase 5 because games are bigger than apps.
You’ll build classic arcade games (Pong, Snake, Asteroids) and then your own game for the milestone.
Bring your machine and your enthusiasm. See you in Phase 6.
If you missed this session
Two cases:
Missed only the demo half: Show your app to Mr. Eric at the start of next week’s class. Same kind of feedback you’d have gotten in the demo.
Missed the whole session: No big deal. Finish your app at home using your Session 7 plan, and bring it next week. Or just join us in Phase 6 — you’ve already shipped enough this phase to be proud.
Stretch and extension ideas
If you have time after demos, or want to keep building at home:
- Polish more. Add features, more widgets, better styling.
- Package it. Tools like PyInstaller can turn your
Python file into a standalone
.exeor.appthat runs without Python installed. Advanced — ask Mr. Eric. - Share it. Send the folder to a friend (with Python installed) and have them try it. Real users, real feedback.
- Write a longer README — installation instructions, examples, screenshots, future ideas.
- Save a copy of every milestone project. They’re yours. Five so far.
What’s next
Phase 6 — games with Pygame. See you there.
Phase 6 — Pygame (2D games)
You can write desktop apps. You can save versions of your work and (soon) push them to the world. Now we make games — animated, interactive, with graphics and sound and the kind of “one more try” pull that real games have.
What this phase is
About fourteen sessions on building 2D games with Pygame. You’ll learn the frame loop — the heartbeat of every video game ever made — and use it to build classic games together (Pong is the big one), then design and ship your own game for the milestone.
This phase is the longest one yet because games are bigger than apps. There’s more to remember, more moving parts, and more chances to get something wrong in a really visible way (sprites flying off-screen, characters walking through walls). That’s also what makes it fun.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to Pygame — the frame loop | The 60-frames-per-second mental model |
| 2 | Drawing on the screen | pygame.draw, colors, shapes, the coordinate system |
| 3 | Sprites and images | Loading PNGs, blit, the Rect class |
| 4 | Movement and the keyboard | Key events + Clock for smooth motion |
| 5 | Collision detection | Rect.colliderect, simple physics |
| 6 | Build Pong together | Your first complete game |
| 7 | GitHub — push to the world | git push, repos as portfolios |
| 8 | Sound and music | pygame.mixer, sound effects, background music |
| 9 | Sprite classes and groups | The Sprite class, organizing many things |
| 10 | The grid-world (intro) | Mr. Eric’s coding-puzzle game — play it |
| 11 | The grid-world (extending) | Build your own puzzles, add features |
| 12 | Title screens and game over | Game state, scenes |
| 13 | Milestone day 1 | Plan + start your game |
| 14 | Milestone day 2 + demo | Finish + showcase |
The Python skills here build on Phase 4’s classes (sprites are classes), Phase 4’s Git (you’ll push your first repo), and Phase 5’s event-driven thinking (rethought as the frame loop).
What you’ll build
- Sessions 1-5: small experiments — moving shapes, sprites you can control, things that bounce.
- Session 6: Pong — a complete game we build as a class.
- Sessions 9-12: the grid-world (a programmable character on a grid solving puzzles), plus polish like title screens.
- Sessions 13-14: your milestone game — your idea, your art, your gameplay.
What you’ll need
- Same machine as before. Pygame is pre-installed on the class machine.
- For working at home, install with:
$ pip install pygame - Thonny still works as your editor.
- A GitHub account (we’ll set this up together in Session 7 — bring an email address you use).
How sessions work
Same shape as before:
- Part A introduces a new game-building concept with a guided exercise.
- Part B is open practice or a project.
- Wrap-up to share what you did.
A note about games
Games are different from anything you’ve built so far.
A Pygame program runs a frame loop — about 60 times every second, the program:
- Reads input (keys, mouse).
- Updates state (move things, check collisions).
- Draws everything to the screen.
That’s the heartbeat. Every game you’ve ever played — Mario, Minecraft, Fortnite — runs this same loop. The art is more elaborate, the physics are more complex, but the structure is the same.
You’re learning the actual mental model that real game developers use, just with simpler graphics.
A note about sprites and classes
Phase 4 introduced classes. Phase 5 used them as a production-style refactor. Phase 6 makes them essential. Every moving thing in your game — the player, each enemy, each bullet, each particle — will be a sprite instance. Sprite groups let you update or draw hundreds of them at once.
If classes felt abstract before, they will not by the end of this phase.
A note about the difficulty curve
Sessions 1-5 are individual mechanics — drawing, moving, collisions. Each one stands alone. Session 6 (Pong) is the first big jump — combining all five into one game. Expect that to feel hard. That’s normal.
After Pong, the curve flattens. Sound, classes, the grid-world — each one adds a new tool but the shape of a game program is now familiar.
Where to start
Session 1: Welcome to Pygame opens your first window — but this time, the window has a heartbeat.
When you’re stuck, the Getting unstuck appendix is the first place to go. The Glossary will grow as Phase 6 introduces game-specific terms.
Welcome to game development. Let’s go.
Session 1: Welcome to Pygame — the frame loop
Session 1: Welcome to Pygame — the frame loop
Phase 6 — Pygame · Session 1 of 14
What we’re learning today
A Pygame program is different from anything you’ve built so far. It runs a frame loop — about 60 times every second, the program reads input, updates the world, and redraws the screen. Today you’ll see your first Pygame window, learn the heartbeat that drives every game ever made, and make a colored rectangle bounce around inside it.
You’ll need to remember from last time
- Importing modules — Phase 4 Session 3.
- Lists, variables, conditionals, functions — Phase 3.
- The event loop from customtkinter (Phase 5) — Pygame has something similar but very different. We’ll compare.
- Running Python from a file —
python game.pyfrom the terminal, or hit Run in Thonny.
Part A: Your first Pygame window
What’s a frame loop?
Open a video game on your phone. The screen is being redrawn about 60 times every second. You don’t see those redraws because they happen so fast — your eye sees smooth animation.
Each “redraw” is a frame. A 60-frame-per-second (60 FPS) game runs the same loop 60 times a second:
- Read input. What keys are pressed? What did the mouse just do?
- Update the world. Move characters, check collisions, advance the score.
- Draw the screen. Clear it, then draw everything in its new position.
That loop is the heartbeat of every video game. Mario, Minecraft, Fortnite — all of them. The art is fancier; the loop is the same.
Compare to Phase 5 customtkinter:
| customtkinter | Pygame |
|---|---|
| Sits and waits for the user | Runs constantly at 60 FPS |
| Calls a function when something happens | Reads input every frame |
| You don’t draw — widgets draw themselves | You draw everything yourself, every frame |
Same Python, very different mental model.
The minimum Pygame program
Open Thonny. Save a new file as game.py. Type this:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("My first Pygame")
clock = pygame.time.Clock()
running = True
while running:
# 1. Read input
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 2. Update the world
# (nothing yet)
# 3. Draw the screen
screen.fill((30, 30, 40)) # dark blue-grey
pygame.display.flip() # show what we drew
clock.tick(60) # cap at 60 FPS
pygame.quit()
Save. Run. A window appears with a dark blue-grey background. Click the X to close it.
That’s it. You have a running game. It’s empty, but the heartbeat is there.
What every line does:
import pygame— bring in the Pygame library.pygame.init()— start up Pygame. Always at the top.screen = pygame.display.set_mode((600, 400))— create a 600-wide-by-400-tall window. Thescreenis what we’ll draw on.pygame.display.set_caption(...)— sets the window title bar.clock = pygame.time.Clock()— keeps time so we can cap the frame rate.while running:— the frame loop. Each pass is one frame.for event in pygame.event.get():— pulls all input events that happened since last frame (key presses, clicks, the close-window X).if event.type == pygame.QUIT:— the user clicked the X. Stop looping.screen.fill((30, 30, 40))— paint the whole screen this color (RGB). Always do this first — it erases the previous frame so we can draw fresh.pygame.display.flip()— show what we drew. Without this, you’d see nothing.clock.tick(60)— wait just long enough so we run at 60 frames per second, no faster.pygame.quit()— clean up when the loop ends.
Memorize this shape. Every Pygame program has it.
Add a rectangle
Inside the loop, after screen.fill(...) but before
pygame.display.flip(), add:
pygame.draw.rect(screen, (255, 100, 50), (250, 150, 100, 100))
Save. Run. An orange-red square appears in the middle.
The arguments:
screen— what to draw on.(255, 100, 50)— color, RGB. (Red 255, green 100, blue 50.) We’ll talk about colors next session.(250, 150, 100, 100)— the rectangle: x, y, width, height. So at position (250, 150), 100 wide and 100 tall.
That last group (x, y, w, h) is a Rect — Pygame’s
basic shape. You’ll use these constantly.
A quick word on coordinates
Pygame’s coordinate system isn’t quite what you might expect:
(0, 0)is the top-left corner.- x increases to the right.
- y increases downward.
So (250, 150) is 250 pixels right of the left edge and
150 pixels down from the top edge.
This is different from the math class graphs you’ve seen (where y goes up). It’s the same as Phase 1’s Scratch was, roughly — most computer graphics do it this way.
Make it move
Now the heartbeat starts to matter. Replace the hardcoded
250 with a variable that changes every frame.
Above the loop, add:
x = 250
In the update part of the loop (right after the event handling, before the draw), add:
x = x + 2
In the draw, change the rect to use x:
pygame.draw.rect(screen, (255, 100, 50), (x, 150, 100, 100))
Save. Run. The rectangle slides right across the screen, off the edge, gone forever.
Why? Each frame, x grows by 2. After about 5 seconds at
60 FPS, the rectangle is way past the right edge.
Make it bounce
Add another variable for x-velocity:
x = 250
dx = 2 # how much to move x each frame
In the update step:
x = x + dx
# bounce off left and right edges
if x < 0 or x > 500: # 600 - 100 (width of rect)
dx = -dx
Save. Run. The rectangle now bounces back and forth.
Each frame:
- Move
xbydx(which is 2, then maybe -2, then 2…). - If
xis off either edge, flip the direction (negatedx).
That’s an animation. You wrote it. You’re a game developer now.
Checkpoint: You have a Pygame window with a bouncing rectangle. This is the natural stop point if class is cut short.
Part B: Make it your own
The base goal: tweak the bouncing rectangle so it does something noticeably yours. Pick one or more:
Stretch — bounce vertically too
Add y and dy. Update both each frame. Bounce off top
and bottom too.
x = 250
y = 150
dx = 2
dy = 1.5
In update:
x = x + dx
y = y + dy
if x < 0 or x > 500:
dx = -dx
if y < 0 or y > 300: # 400 - 100
dy = -dy
Now the rectangle bounces around the whole window diagonally. Looks like the old DVD player screensaver.
Stretch — change color on bounce
Above the loop, add a list of colors:
colors = [(255, 100, 50), (50, 255, 100), (100, 50, 255), (255, 255, 100)]
color = colors[0]
import random
When bouncing, pick a new color:
if x < 0 or x > 500:
dx = -dx
color = random.choice(colors)
Then draw with the variable: pygame.draw.rect(screen, color, ...).
Stretch — multiple bouncing rectangles
Use a list of [x, y, dx, dy, color] lists. Loop through
them in update and draw.
Extension — speed up over time
Each bounce, slightly increase dx and dy (multiply by
1.05 or so). Watch it accelerate. Add a “max speed” cap.
Extension — change with the keyboard
Below the QUIT check (still inside the for-event loop), add:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
dx = -dx
dy = -dy
Now pressing space reverses direction.
(We’ll go deeper into keyboard input in Session 4.)
Wrap-up
Before we leave, share with the room:
- For everyone — show your bouncing rectangle.
- Did you tweak it? What did you change?
- Does the “60 frames per second” feel make sense? Anyone notice that the motion looks smooth?
Today you wrote your first frame loop. The heartbeat of every video game. Read input, update the world, draw the screen. You’ll see this shape in every Pygame program for the rest of the phase.
You also learned:
pygame.init(),set_mode,Clock, thewhile runningloop,pygame.display.flip().- The Pygame coordinate system (
(0, 0)top-left). pygame.draw.rectand the(x, y, w, h)shape.- Animation = changing values across frames.
- Bouncing = flipping a velocity when you hit a boundary.
Keep this game.py. We’ll use it as a starting point in
later sessions.
If you missed this session
Open Thonny. Then:
-
Type out the minimum Pygame program. Run it. See the blank window.
-
Add the orange rectangle. Run it. See the rectangle.
-
Make it move (
x = x + 2). Run it. See it slide off the screen. -
Make it bounce (add
dx, flip on edges). Run it.
If you don’t have Pygame at home, install it from a terminal:
$ pip install pygame
About 30-45 minutes. By the end you should have a bouncing rectangle.
Stretch and extension ideas
- Bounce in 2D. Both x and y, with
dxanddy. - Color changes. New random color on each bounce.
- Many rectangles. A list of them, all bouncing.
- Different shapes.
pygame.draw.circle(screen, color, (cx, cy), radius)andpygame.draw.line(...). Try drawing a bouncing circle instead. - Trail effect. Don’t fully clear the screen — fill
with a translucent color. Use
pygame.Surface((600, 400), pygame.SRCALPHA)thensurface.fill((30, 30, 40, 20))and blit. Advanced. - Different background. Try
screen.fill((255, 255, 255))for white, or any other RGB. - Slow motion. Change
clock.tick(60)toclock.tick(10). Watch the rectangle move in slow, choppy hops.
What’s next
Next week is drawing on the screen — circles, lines, polygons, all the colors. We’ll build a small scene by combining shapes (a face, a house, a landscape — your pick).
Session 2: Drawing on the screen
Session 2: Drawing on the screen
Phase 6 — Pygame · Session 2 of 14
What we’re learning today
Last week you drew rectangles. Today we add the rest of Pygame’s drawing toolkit: circles, lines, polygons, text, and all the colors. By the end of class you’ll have built a small scene — a face, a house, a landscape, or whatever you want — by combining shapes.
You’ll need to remember from last time
- The frame loop — read input, update, draw, repeat.
pygame.draw.rect(screen, color, (x, y, w, h)).- The coordinate system —
(0, 0)is top-left, y goes down. screen.fill(color)to clear the screen each frame.pygame.display.flip()to show what we drew.
Part A: Shapes and colors
Start fresh
Open Thonny. Save a new file as scene.py. Type the same
minimum Pygame program as last week:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("My Scene")
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((180, 220, 255)) # light blue (sky?)
# ← we'll draw shapes here
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. Window with a light blue background.
Colors are RGB triples
Every color in Pygame is (red, green, blue) — three numbers from 0 to 255.
(0, 0, 0)— black (no light)(255, 255, 255)— white (all light)(255, 0, 0)— pure red(0, 255, 0)— pure green(0, 0, 255)— pure blue(255, 255, 0)— yellow (red + green)(255, 0, 255)— magenta (red + blue)(0, 255, 255)— cyan (green + blue)(128, 128, 128)— middle grey(180, 220, 255)— pale sky blue (what we used above)
Mix the three values to get any color. Want orange? More
red, some green, no blue: (255, 165, 0). Want forest
green? Less green, a little red and blue:
(34, 100, 30). Want pink? Lots of red, some green and
blue: (255, 180, 200).
If you want to look up specific colors, search “RGB picker” — every browser has one online.
You can also use a 4th value for transparency (alpha)
— but that requires Surface tricks. We’ll skip it for
now.
Rectangles (review)
pygame.draw.rect(screen, color, (x, y, width, height))
The position (x, y) is the top-left corner. Width
and height go right and down from there.
pygame.draw.rect(screen, (200, 100, 50), (250, 250, 100, 100))
That’s a brown 100×100 square with its top-left at (250, 250).
To draw an outline only (not filled), add a fifth argument — the line width:
pygame.draw.rect(screen, (255, 255, 255), (100, 100, 80, 80), 3)
That’s a white square outline, 3 pixels thick, hollow inside.
Circles
pygame.draw.circle(screen, color, (center_x, center_y), radius)
The position is the center (not the top-left like rects). The radius is the size.
pygame.draw.circle(screen, (255, 220, 0), (300, 100), 40)
A yellow circle of radius 40, centered at (300, 100). Think: a sun.
Outline-only also works:
pygame.draw.circle(screen, (0, 0, 0), (300, 100), 40, 2)
Lines
pygame.draw.line(screen, color, (x1, y1), (x2, y2), width)
Draws a line from (x1, y1) to (x2, y2).
pygame.draw.line(screen, (50, 50, 50), (0, 350), (600, 350), 4)
A dark grey line, 4 pixels thick, from the left edge to the right edge at y=350. Think: the ground.
Polygons
For triangles or other shapes, use polygons — a list of points:
pygame.draw.polygon(screen, color, [point1, point2, point3, ...])
Each point is (x, y). The polygon connects them in
order and closes the shape.
pygame.draw.polygon(screen, (200, 50, 50), [(250, 200), (350, 200), (300, 150)])
A red triangle (a roof, maybe?) with corners at three specific points.
Try it together — a tiny scene
Replace your “we’ll draw shapes here” comment with:
# ground
pygame.draw.line(screen, (50, 100, 50), (0, 350), (600, 350), 6)
# house body
pygame.draw.rect(screen, (200, 150, 100), (250, 250, 100, 100))
# house roof
pygame.draw.polygon(screen, (150, 50, 50), [(240, 250), (360, 250), (300, 180)])
# door
pygame.draw.rect(screen, (80, 50, 30), (290, 300, 20, 50))
# sun
pygame.draw.circle(screen, (255, 220, 0), (500, 80), 35)
Save. Run. A tiny house in a meadow under a sun.
That’s it — that’s how you make scenes in Pygame. Each line is one shape. Order matters: things drawn later appear on top of things drawn earlier. (Notice the door is drawn after the house body — that’s why you see it.)
Checkpoint: You have a scene with at least three different shapes and three different colors. This is the natural stop point if class is cut short.
Part B: Build your own scene
Time to make something yours. Pick a subject and build it out of shapes:
- A face (circle for head, smaller circles for eyes, rectangle or polygon for mouth, lines for eyebrows)
- A house (extend the example with windows, chimney, trees, clouds)
- A landscape (mountains as polygons, sun, river, trees)
- A rocket ship (rectangles, triangles, circles for windows)
- A robot (rectangles for body and head, circles for eyes, lines for antennas)
- A cat (circles, triangles, lines for whiskers)
- A city skyline (rectangles of varying heights)
- An abstract pattern (circles in a grid, color gradients)
Spend at least 20 minutes. Use at least 6 shapes. Use at least 3 colors. Make it look like something — even a recognizable smiley face beats a pile of random shapes.
Stretch — write text on the screen
Pygame can draw text. The setup is fiddlier than for shapes:
# above the loop, once:
font = pygame.font.SysFont("Arial", 36)
# inside the loop, in the draw section:
text_surface = font.render("Hello!", True, (0, 0, 0))
screen.blit(text_surface, (50, 50))
What that does:
pygame.font.SysFont("Arial", 36)— get the system Arial font at size 36.font.render("Hello!", True, (0, 0, 0))— render the text.Truemeans anti-aliased (smooth edges). The color is RGB.screen.blit(text_surface, (50, 50))— paste the rendered text at position (50, 50).blitis “draw this image onto the screen.”
Add a title or label to your scene. Try different fonts:
"Times New Roman", "Courier", "Comic Sans MS".
Stretch — animate one thing
Last week you bounced a rectangle. This week, animate one piece of your scene:
- A bird (small triangle) flying across.
- A sun moving across the sky.
- A car (rectangle on rectangle wheels) driving past.
- A bouncing ball.
Use the same pattern: a variable that changes each frame, used in the position when you draw.
Extension — interactive scene
Make the scene respond to the mouse:
# inside the loop:
mouse_x, mouse_y = pygame.mouse.get_pos()
pygame.draw.circle(screen, (255, 100, 100), (mouse_x, mouse_y), 20)
Now a red circle follows your cursor. Add it to your scene — a “you are here” pointer.
Extension — gradient background
A real sky has a gradient (lighter at the horizon, darker above). Loop through y-values, drawing a thin horizontal line of a slightly different color at each row:
for y in range(400):
blue = 255 - y // 4 # 255 at top, decreasing
pygame.draw.line(screen, (180, 200, blue), (0, y), (600, y))
Replace your screen.fill(...) with that.
Wrap-up
Before we leave, share with the room:
- For everyone — show your scene. What did you build?
- For the kids who animated something — does it look alive?
- Anyone find a particularly satisfying color combo?
- Did you discover the rule about draw order? (Things drawn later appear on top.)
Today you learned the drawing toolkit:
- Rectangles with
pygame.draw.rect. - Circles with
pygame.draw.circle. - Lines with
pygame.draw.line. - Polygons with
pygame.draw.polygon. - Text with
font.render+screen.blit(stretch). - Colors as
(r, g, b)triples. - Draw order — later shapes go on top.
Combined with last week’s frame loop, you can now make any visual you can imagine. The only limit is shapes and colors — which means the limit is your imagination.
If you missed this session
Open Thonny. Then:
-
Type the minimum Pygame program. Run it.
-
Draw a rectangle, a circle, a line, and a polygon. Run after each to make sure it appears.
-
Pick a subject (house, face, robot, etc.) and build a small scene with at least 6 shapes.
About 30-45 minutes. By the end you should have a scene made of shapes.
Stretch and extension ideas
- Text labels for parts of your scene.
- Animate one piece.
- Mouse-following shape.
- Gradient background.
- More shape types —
pygame.draw.ellipse(screen, color, rect)(oval inside a rect),pygame.draw.arc(...)(partial circle),pygame.draw.aaline(...)(smoother lines). - Save your scene as an image:
pygame.image.save(screen, "scene.png")writes a PNG file. Add this where you want a snapshot — maybe on a keypress. - Random scenes: use
random.randint(...)to pick positions and colors. Each run is a different artwork.
What’s next
Next week we move from drawing shapes to loading images. You’ll bring in a PNG file, slap it on the screen, and use it as a sprite — the building block of every game with characters, enemies, and items.
Session 3: Sprites and images
Session 3: Sprites and images
Phase 6 — Pygame · Session 3 of 14
What we’re learning today
You can draw shapes. That’s enough for prototypes — but real games use images. A character isn’t a rectangle; it’s a picture. Today you’ll load a PNG file into your program, paste it on the screen, and move it around. By the end you’ll have your first sprite — a moving image with a position. This is the building block of every game with characters, enemies, items, and projectiles.
You’ll need to remember from last time
- The frame loop (Session 1).
- Drawing shapes (Session 2).
- The coordinate system —
(0, 0)top-left, y goes down. - Variables that change each frame = animation.
- Lists (Phase 3 Session 8) — for groups of sprites.
Part A: Loading and drawing an image
Get an image
In class today, you’ve been given some sprite PNGs in a
folder named images/. If you’re working at home, find
or make a small PNG (you can draw one in any image
editor, or download a free game sprite).
A good starter sprite is transparent — the background isn’t a solid color. PNGs support transparency; JPGs don’t. Stick with PNGs.
Put the image file in the same folder as your Python file.
A program to display it
Open Thonny. Save a new file as sprite.py in the same
folder as your image. Type:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("My first sprite")
clock = pygame.time.Clock()
# Load the image — once, before the loop
player_image = pygame.image.load("player.png")
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((100, 150, 100)) # green background
# Draw the image at position (300, 200)
screen.blit(player_image, (300, 200))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Replace "player.png" with the actual filename of your
image.
Save. Run. Your image appears at position (300, 200).
What’s new:
pygame.image.load("player.png")— reads the file from disk and creates aSurface(Pygame’s word for “image you can draw”).screen.blit(image, (x, y))— pastes the image onto the screen at position(x, y). The position is the top-left corner of the image (just like rects).
blit (rhymes with “fit”) is short for “block transfer.”
It’s how you paste one image onto another. The screen
is also a Surface, so we blit images onto it.
Load images once, draw them every frame
Notice pygame.image.load is before the loop, but
blit is inside it.
This is important. Loading an image from disk is slow — if you reloaded it every frame, your game would crawl. Load once, blit many times.
Same with the screen itself, the clock, and any other
setup. Set up once, use every frame.
Move the sprite
Same trick as the bouncing rectangle. Add variables for position, change them each frame.
Add above the loop:
x = 300
y = 200
Change the blit:
screen.blit(player_image, (x, y))
In the update part of the loop:
x = x + 2
Save. Run. The sprite moves right, off the edge.
The Rect — Pygame’s box
Every image (and shape) in Pygame has a Rect —
a rectangle that describes its position and size. Rects
make collision detection (next session) easy and they
keep position bookkeeping clean.
Get a Rect from an image:
player_rect = player_image.get_rect()
print(player_rect) # Rect(0, 0, width, height)
The default Rect has its top-left at (0, 0) and the
size of the image. You can move it:
player_rect = player_image.get_rect()
player_rect.x = 300
player_rect.y = 200
Then blit using the rect:
screen.blit(player_image, player_rect)
blit accepts either a (x, y) tuple or a Rect. Same
result — but with the Rect, you have a bunch of useful
properties:
player_rect.xandplayer_rect.y— top-leftplayer_rect.widthandplayer_rect.height— sizeplayer_rect.right—x + widthplayer_rect.bottom—y + heightplayer_rect.center—(cx, cy)of the rectplayer_rect.centerx,player_rect.centeryplayer_rect.move_ip(dx, dy)— move “in place” by(dx, dy)
Try changing player_rect.x = ... to:
player_rect.center = (300, 200)
Now the image is centered at (300, 200) instead of having its top-left there. Often what you want.
Update with the Rect
Move the rect each frame instead of separate variables:
player_rect.x = player_rect.x + 2
Or, more idiomatic:
player_rect.move_ip(2, 0)
move_ip moves the rect “in place” — modifies it
directly. Same effect, less typing.
Checkpoint: You have a sprite (image) on the screen that you can move with a Rect. This is the natural stop point if class is cut short.
Part B: Many sprites and a small scene
Time to build a small game scene with multiple sprites.
Multiple sprites
Each sprite needs its own image and its own Rect.
player_image = pygame.image.load("player.png")
enemy_image = pygame.image.load("enemy.png")
coin_image = pygame.image.load("coin.png")
player_rect = player_image.get_rect(center=(100, 200))
enemy_rect = enemy_image.get_rect(center=(500, 200))
coin_rect = coin_image.get_rect(center=(300, 200))
Note the trick: get_rect(center=(100, 200)) immediately
positions the rect with that center. Saves two lines.
In the draw section:
screen.blit(player_image, player_rect)
screen.blit(enemy_image, enemy_rect)
screen.blit(coin_image, coin_rect)
A list of sprites — for groups
When you have many of the same kind of thing (10 enemies, 20 coins, 100 stars), use a list of rects:
import random
# Make 10 random coin positions
coins = []
for i in range(10):
cx = random.randint(0, 600)
cy = random.randint(0, 400)
coin_rect = coin_image.get_rect(center=(cx, cy))
coins.append(coin_rect)
Then in the draw section:
for coin_rect in coins:
screen.blit(coin_image, coin_rect)
One image, many rects, drawn in a loop. This is how real games handle many objects.
Animate the player
Combine everything. Use mouse position to move the player:
# inside the loop, in update:
mouse_x, mouse_y = pygame.mouse.get_pos()
player_rect.center = (mouse_x, mouse_y)
Now the sprite follows your mouse around. Move over the coins. Notice they don’t react yet — collision detection is next session.
Stretch — scaling and rotating
Pygame can resize and rotate images:
# Make the player twice as big
player_image = pygame.transform.scale(player_image, (128, 128))
# Rotate 45 degrees (counter-clockwise)
rotated = pygame.transform.rotate(player_image, 45)
scale takes (width, height). rotate takes degrees
(positive = counter-clockwise).
Note: rotated images often change size (the box around the rotated image is bigger). You may need to recompute the rect.
Stretch — flipping
flipped_image = pygame.transform.flip(player_image, True, False)
The two booleans are (flip_x, flip_y). Useful for making
your sprite face left vs right when moving.
Extension — sprite trail
Don’t update player_rect.center = ... directly. Instead,
keep a list of recent positions and draw the sprite at
each one with decreasing transparency. Advanced — requires
Surface alpha.
Wrap-up
Before we leave, share with the room:
- For everyone — show your sprite. Did you use the one from class, or did you bring your own?
- For the kids who built the multi-sprite scene — show it.
- For the kids who tried scaling/rotating — does it look cool?
- Anyone notice the coins don’t react when the player touches them? (Foreshadowing.)
Today you learned:
pygame.image.load(filename)to read PNGs.screen.blit(image, position)to paste an image.- Load once, blit every frame.
image.get_rect()to get a Rect for an image.- Rect properties —
x,y,width,height,center,right,bottom, etc. get_rect(center=(x, y))to position by center.- Lists of sprites — one image, many rects.
- (Stretch)
pygame.transform.scale,rotate,flip.
You’ve gone from “shapes drawn from code” to “real game art on screen.” The visual gap from Session 2 to today is huge. Next week we make these sprites respond to keys.
If you missed this session
Open Thonny. Get a PNG image (any small one — a dot, a square, anything from the internet). Then:
-
Type the minimum Pygame program. Add the
image.loadandblitlines to display your image. -
Switch to using a Rect (
get_rect(),screen.blit(img, rect)). -
Move the sprite — try
mouse.get_pos()to follow the mouse. -
Add a second sprite (different image or same image).
About 30-45 minutes. By the end you should have multiple sprites on screen.
Stretch and extension ideas
- Scale, rotate, flip images.
- Mouse-controlled sprite.
- Random sprite placement — a starfield, scattered coins, falling snowflakes.
- Background image — load a big PNG, blit it at
(0, 0)first. Background scenery. - Sprite trail (advanced — alpha surfaces).
- Animated sprite — load multiple images of the same character in different poses, switch between them each few frames. Walking animation.
- Resize while running — make the sprite grow when you
press space (modify the
scalecall).
What’s next
Next week we add input — keyboard controls. You’ll make the player sprite move with WASD or the arrow keys, which is the foundation for almost any game with a playable character.
Session 4: Movement and the keyboard
Session 4: Movement and the keyboard
Phase 6 — Pygame · Session 4 of 14
What we’re learning today
You can put a sprite on screen. Today we make it listen. Specifically, to the keyboard. By the end you can move a sprite around with WASD or arrow keys — and you’ll understand the two ways Pygame handles input (events and continuous polling) and when to use each. This is the foundation for almost every game with a playable character.
You’ll need to remember from last time
- Sprites —
pygame.image.load,screen.blit, theRectclass. get_rect(center=(x, y))to position by center.rect.move_ip(dx, dy)to move a rect.- The frame loop — read input, update, draw.
- Conditionals —
if,elif,else.
Part A: Two ways to read the keyboard
Pygame gives you two different APIs for the keyboard, and they’re for different jobs. Knowing when to use each saves real bugs.
Way 1: Events — for moments
Last sessions you used:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
The for event in pygame.event.get() loop reads every
event that happened since the last frame. The user
clicked? Event. User pressed a key? Event. User released
a key? Event.
Use events when you care about a moment — the instant something happened. Examples:
- “When the user presses space, fire a bullet.” (One bullet per press, not 60 per second.)
- “When the user presses escape, pause the game.”
- “When the user clicks the mouse, drop a marker.”
Add to the event loop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
print("Space was just pressed!")
if event.key == pygame.K_ESCAPE:
running = False
KEYDOWN is the moment a key starts being pressed.
KEYUP is the moment it’s released. event.key tells
you which key.
The constants are pygame.K_a, pygame.K_b, …
pygame.K_SPACE, pygame.K_ESCAPE, pygame.K_LEFT,
pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN,
pygame.K_RETURN (enter), and so on.
Way 2: Polling — for “is it down right now?”
For continuous actions like “move while the key is held,” events don’t work cleanly. (Holding a key only sends one KEYDOWN, plus repeats much later.)
Instead, poll the keyboard each frame:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
player_rect.move_ip(-5, 0)
if keys[pygame.K_RIGHT]:
player_rect.move_ip(5, 0)
pygame.key.get_pressed() returns a special dict-like
object. Index into it with a key constant — get True
(currently pressed) or False (not pressed).
This runs every frame. Hold the left arrow → 60 moves per second. Release → stop.
Rule of thumb
- One-shot action? Use event. (Fire, pause, jump.)
- Continuous action? Use
get_pressed. (Move, hold, charge.)
Build it — a movable sprite
Open Thonny. Save a new file as movement.py. Type:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Move me!")
clock = pygame.time.Clock()
player_image = pygame.image.load("player.png")
player_rect = player_image.get_rect(center=(300, 200))
SPEED = 5
running = True
while running:
# 1. Read input — events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
# 2. Read input — held keys
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
player_rect.x -= SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
player_rect.x += SPEED
if keys[pygame.K_UP] or keys[pygame.K_w]:
player_rect.y -= SPEED
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
player_rect.y += SPEED
# 3. Draw
screen.fill((100, 150, 100))
screen.blit(player_image, player_rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Replace "player.png" with your sprite filename.
Save. Run. Press the arrow keys or WASD. The sprite moves. Press escape to quit.
What’s new:
- Both WASD and arrows work —
if keys[A] or keys[B]. Real games support multiple control schemes. SPEED = 5at the top. A constant — uppercase signals “this is a value I might tune.” Easy to find and change.- Negative y for up. Remember y goes down; subtract to move up.
player_rect.x -= SPEEDis shorthand forplayer_rect.x = player_rect.x - SPEED.
Stay on screen
Right now you can walk off the edge and disappear. Add boundary checks:
# After all the movement code, before draw:
if player_rect.left < 0:
player_rect.left = 0
if player_rect.right > 600:
player_rect.right = 600
if player_rect.top < 0:
player_rect.top = 0
if player_rect.bottom > 400:
player_rect.bottom = 400
Notice we use left, right, top, bottom — Rect
properties from Session 3. Setting left = 0 snaps the
rect’s left edge to 0.
Save. Run. The sprite stops at the edges. Stays on screen, no matter how long you hold the key.
Checkpoint: You have a sprite that moves with WASD or arrow keys, stays on screen, and quits with escape. This is the natural stop point if class is cut short.
Part B: A small game — collect the items
Time to combine sprites + keyboard + lists into a tiny game. The base goal: a player you control, a few items scattered on screen, and a way to know if you “got” one (by walking on top of it).
We’ll do the touching part properly next session (collision detection). For today, we’ll fake it with a simple distance check.
Build it
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Collector")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 24)
player_image = pygame.image.load("player.png")
coin_image = pygame.image.load("coin.png")
SPEED = 5
player_rect = player_image.get_rect(center=(50, 200))
# Make 8 coins at random positions
coins = []
for i in range(8):
cx = random.randint(50, 550)
cy = random.randint(50, 350)
coin_rect = coin_image.get_rect(center=(cx, cy))
coins.append(coin_rect)
score = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
# Movement
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
player_rect.x -= SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
player_rect.x += SPEED
if keys[pygame.K_UP] or keys[pygame.K_w]:
player_rect.y -= SPEED
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
player_rect.y += SPEED
# Stay on screen
player_rect.clamp_ip(screen.get_rect())
# Check for "collection" — distance check
for coin_rect in coins[:]: # iterate over a copy
dx = player_rect.centerx - coin_rect.centerx
dy = player_rect.centery - coin_rect.centery
if dx * dx + dy * dy < 40 * 40: # within 40 pixels
coins.remove(coin_rect)
score += 1
# Draw
screen.fill((100, 150, 100))
for coin_rect in coins:
screen.blit(coin_image, coin_rect)
screen.blit(player_image, player_rect)
score_surface = font.render(f"Score: {score}", True, (255, 255, 255))
screen.blit(score_surface, (10, 10))
if len(coins) == 0:
win_surface = font.render("You got them all!", True, (255, 255, 0))
screen.blit(win_surface, (200, 200))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. Walk around and collect all the coins. Score goes up. When you collect the last one, “You got them all!” appears.
Quite a bit going on:
player_rect.clamp_ip(screen.get_rect())— keeps the player rect inside the screen rect. Cleaner than the fourifboundaries from Part A.coins[:]—[:]makes a copy of the list. We loop over the copy because we’re removing from the original inside the loop. Modifying a list while looping over it directly causes weird bugs.- Distance check:
dx*dx + dy*dy < 40*40is the Pythagorean-without-square-root version of “within 40 pixels.” Faster than callingmath.sqrt. f"Score: {score}"— f-string from Phase 3, drawn withfont.render.- Win text when no coins remain.
You just built a tiny game with a player, items, scoring, and a win condition. You’re a game designer.
Stretch — add enemies
Add a few enemy sprites that move on their own. If the player gets too close, lose.
enemy_image = pygame.image.load("enemy.png")
enemies = []
for i in range(3):
ex = random.randint(100, 500)
ey = random.randint(100, 300)
enemy_rect = enemy_image.get_rect(center=(ex, ey))
enemies.append([enemy_rect, random.choice([-2, 2]), random.choice([-2, 2])])
# Inside the loop, in update:
for enemy in enemies:
enemy_rect, edx, edy = enemy
enemy_rect.x += edx
enemy_rect.y += edy
if enemy_rect.left < 0 or enemy_rect.right > 600:
enemy[1] = -edx
if enemy_rect.top < 0 or enemy_rect.bottom > 400:
enemy[2] = -edy
# Did the enemy catch the player?
dx = player_rect.centerx - enemy_rect.centerx
dy = player_rect.centery - enemy_rect.centery
if dx*dx + dy*dy < 40*40:
running = False
print("You lose!")
# Draw enemies in the draw section:
for enemy_rect, _, _ in enemies:
screen.blit(enemy_image, enemy_rect)
Now you have a game with risk and reward.
Stretch — diagonal speed fix
If you press left + up at the same time, the player moves faster diagonally than horizontally. (Think Pythagoras — moving 5 in x AND 5 in y is moving ~7 along the diagonal.)
A simple fix: normalize the diagonal speed.
import math
dx, dy = 0, 0
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
dx -= 1
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
dx += 1
if keys[pygame.K_UP] or keys[pygame.K_w]:
dy -= 1
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
dy += 1
# Normalize diagonal
if dx != 0 and dy != 0:
dx *= 0.707 # 1 / sqrt(2)
dy *= 0.707
player_rect.x += dx * SPEED
player_rect.y += dy * SPEED
Now diagonal movement is the same speed as straight.
Extension — restart on win
When all coins are collected, refill them and reset the score. Endless mode!
if len(coins) == 0:
for i in range(8):
cx = random.randint(50, 550)
cy = random.randint(50, 350)
coin_rect = coin_image.get_rect(center=(cx, cy))
coins.append(coin_rect)
# could also: SPEED += 1 to make it harder each round
Wrap-up
Before we leave, share with the room:
- For everyone — show your collector. How many coins did you set?
- For the kids who added enemies — was it harder? Did you lose to your own enemies?
- Anyone notice the diagonal-speed bug?
- Did the events vs polling distinction make sense? Can anyone name a one-shot action vs a continuous one?
Today you learned:
- Two input APIs: events (for moments) and
get_pressed(for held keys). - Key constants —
pygame.K_LEFT,pygame.K_a, etc. - WASD or arrows as alternate controls.
- Boundary clamping with
Rect.clamp_ip. - Distance checks as a stand-in for collision (real collision is next session).
- List-while-modifying trick:
for x in coins[:]:. - A complete tiny game — player, items, score, win condition.
Next week we make collisions proper — rect.colliderect
— and that opens up Pong, our first big game build.
If you missed this session
Open Thonny. You’ll need a player sprite and a coin sprite (or any two PNGs).
-
Build the basic movable-player example from Part A first. WASD or arrows. Make sure boundary clamping works.
-
Build the collector game from Part B. Adjust number of coins, position, and SPEED to taste.
-
(Stretch) Add enemies.
About 45-60 minutes. By the end you should have a working collector game.
Stretch and extension ideas
- Enemies that chase the player. Compute direction from enemy to player each frame, move the enemy slightly in that direction.
- Multiple players — WASD for player 1, arrows for player 2, two players, two scores.
- Sprite faces direction. When moving left, flip the
player image. (
pygame.transform.flip.) - Sound effect on collection. (We’ll do sound properly in Session 8.)
- Speed-up over time — each second, increase SPEED.
- Random spawn timing — coins appear over time, not all at once.
- Pause menu — press P to pause; the game state freezes until P is pressed again.
What’s next
Next week: collision detection. The proper way for sprites to know they’ve touched. Then we use it to build Pong.
Session 5: Collision detection
Session 5: Collision detection
Phase 6 — Pygame · Session 5 of 14
What we’re learning today
Last week you faked “touching” with a distance check.
Today you’ll do it properly: Pygame’s built-in
collision methods. Two rects either overlap or they
don’t — rect.colliderect(other) answers in one line.
You’ll use this to build a “falling fruit catcher” mini
game with real collisions, scoring, and lives. By the end
of class you’ll be ready for Session 6 — Pong.
You’ll need to remember from last time
- Sprites and rects (Session 3).
- Keyboard input (Session 4) — events vs polling.
- Lists of rects (Session 3) — many of the same thing.
- The frame loop.
Part A: Real collision detection
rect.colliderect(other_rect)
The simplest collision check in Pygame:
if player_rect.colliderect(coin_rect):
print("Touching!")
Returns True if the two rectangles overlap, False
otherwise. That’s it.
This is rectangle vs rectangle. Pygame compares the boxes — not the actual pixels. If your sprite has empty space inside its rect (most do), the rect catches it before the visible pixel does. That’s normal and fine for most games.
For pixel-perfect collision you’d need pygame.mask —
which we won’t cover. Rect collision is what most games
use.
Two rectangles, one collision
Open Thonny. Save a new file as collision.py. Type:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Collision test")
clock = pygame.time.Clock()
player_image = pygame.image.load("player.png")
coin_image = pygame.image.load("coin.png")
player_rect = player_image.get_rect(center=(300, 200))
coin_rect = coin_image.get_rect(center=(450, 200))
SPEED = 5
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
player_rect.x -= SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
player_rect.x += SPEED
if keys[pygame.K_UP] or keys[pygame.K_w]:
player_rect.y -= SPEED
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
player_rect.y += SPEED
player_rect.clamp_ip(screen.get_rect())
# Check collision
touching = player_rect.colliderect(coin_rect)
# Draw
screen.fill((30, 30, 50) if not touching else (80, 50, 50))
screen.blit(coin_image, coin_rect)
screen.blit(player_image, player_rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. Move the player onto the coin. The background turns red when you’re touching. Move off — back to dark blue.
That’s collision. One line: player_rect.colliderect(coin_rect).
Many things — colliderect in a list
For a list of objects (like the coins from last session), loop through and check each:
for coin_rect in coins[:]: # copy because we modify
if player_rect.colliderect(coin_rect):
coins.remove(coin_rect)
score += 1
There’s also a built-in: rect.collidelist(list_of_rects)
returns the index of the first rect in the list that
collides, or -1 if none. Useful sometimes. The for-loop
version is more flexible (you can react per-collision).
Checkpoint: You have a sprite that moves and reacts when it touches another sprite. This is the natural stop point if class is cut short.
Part B: Falling fruit catcher
A small game. Fruit falls from the top. You move a basket left and right at the bottom. Catch the fruit (collide) — score goes up. Miss — lose a life. Lose 3 lives — game over.
Build it
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600, 500))
pygame.display.set_caption("Fruit catcher")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 28)
basket_image = pygame.image.load("basket.png") # or any sprite
fruit_image = pygame.image.load("fruit.png") # or any sprite
basket_rect = basket_image.get_rect(midbottom=(300, 490))
SPEED = 7
# Each fruit: a rect + falling speed
fruits = []
def make_fruit():
x = random.randint(20, 580)
fruit_rect = fruit_image.get_rect(center=(x, -20))
speed = random.randint(3, 6)
return [fruit_rect, speed]
# Spawn first fruit
fruits.append(make_fruit())
score = 0
lives = 3
spawn_timer = 0
SPAWN_INTERVAL = 60 # frames between spawns (1 second)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if lives > 0:
# Move basket
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
basket_rect.x -= SPEED
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
basket_rect.x += SPEED
basket_rect.clamp_ip(screen.get_rect())
# Move fruits down + check catches/misses
for fruit in fruits[:]:
fruit_rect, speed = fruit
fruit_rect.y += speed
if basket_rect.colliderect(fruit_rect):
fruits.remove(fruit)
score += 1
elif fruit_rect.top > 500:
fruits.remove(fruit)
lives -= 1
# Spawn new fruits
spawn_timer += 1
if spawn_timer >= SPAWN_INTERVAL:
fruits.append(make_fruit())
spawn_timer = 0
# Draw
screen.fill((180, 220, 255))
for fruit_rect, _ in fruits:
screen.blit(fruit_image, fruit_rect)
screen.blit(basket_image, basket_rect)
score_surface = font.render(f"Score: {score}", True, (0, 0, 0))
lives_surface = font.render(f"Lives: {lives}", True, (180, 0, 0))
screen.blit(score_surface, (10, 10))
screen.blit(lives_surface, (480, 10))
if lives <= 0:
over_surface = font.render("GAME OVER", True, (200, 0, 0))
rect = over_surface.get_rect(center=(300, 250))
screen.blit(over_surface, rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. Catch fruit with the basket. Don’t lose all your lives.
What’s new since last session:
get_rect(midbottom=(300, 490))— positions the rect’s bottom-middle at that point. Useful for “stand on the floor” placement.- List of
[rect, speed]pairs — each fruit has its own falling speed. make_fruit()function — encapsulates fruit spawning. Cleaner than inlining the random logic.spawn_timer— counts frames between spawns. Triggersmake_fruit()everySPAWN_INTERVALframes. This is a timer, the game-dev pattern for “do something every N seconds.”- Lives countdown — fruit that falls past the bottom costs a life.
if lives > 0:around the update logic — when lives = 0, the game freezes (no movement, no spawning). Only the draw still runs (so we can show GAME OVER).- Two text labels — score on the left, lives on the right.
You just built a complete arcade-style game with collision, scoring, lives, spawning, and a game-over state. This is a real game.
Stretch — restart on game over
When game over, press R to restart:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and lives <= 0:
# Reset state
lives = 3
score = 0
fruits = []
spawn_timer = 0
Stretch — different fruit types
Use a list of images. Random choice each spawn:
fruit_images = [
pygame.image.load("apple.png"),
pygame.image.load("banana.png"),
pygame.image.load("cherry.png"),
]
def make_fruit():
image = random.choice(fruit_images)
x = random.randint(20, 580)
fruit_rect = image.get_rect(center=(x, -20))
speed = random.randint(3, 6)
return [image, fruit_rect, speed]
Update the loop to handle the extra image value in each
fruit entry, and blit the fruit’s own image instead of a
shared fruit_image.
Stretch — bombs!
Some falling things are bombs — catching one costs a life. Different sprite (a black circle, a red X). When the player catches one, lose a life instead of gaining score.
Extension — speed up over time
Every 10 score, increase fall speed slightly. Or make SPAWN_INTERVAL shorter. The game gets harder as you go — classic arcade design.
Wrap-up
Before we leave, share with the room:
- For everyone — what’s your high score?
- Did the GAME OVER feel real? You lose something now.
- For the kids who added bombs — was it harder or easier than catching?
- Anyone notice that you can move the basket while a fruit is mid-fall? That’s the frame loop in action — every frame, the basket moves AND the fruit falls AND collisions are checked.
Today you learned:
rect.colliderect(other_rect)— the standard collision check in Pygame.- Looping over a list to check collisions — the pattern for many objects.
midbottom,topleft, etc. — positioning by edges and corners.- Spawning over time — frame timers +
randomfor procedural content. - Game state —
livesandscoreas state variables;if lives > 0:to gate the update. - A complete arcade game — collision, lives, scoring, game over.
Today is the conceptual peak of Phase 6 fundamentals. You now have the four core ingredients of any 2D game: draw, move, input, collide. Combine them in different shapes and you get any genre.
If you missed this session
Open Thonny. You’ll need a basket sprite and a fruit sprite (or any two PNGs).
-
Build the basic collision example from Part A. Verify the background changes when you touch the coin.
-
Build the fruit catcher from Part B. Adjust SPEED, SPAWN_INTERVAL, and starting lives to taste.
-
(Stretch) Add bombs or different fruit types.
About 45-60 minutes. By the end you should have a working fruit catcher.
Stretch and extension ideas
- Bombs that cost lives.
- Power-ups — a “double points” fruit that grants 2 score for one catch.
- Combo system — catching 3 in a row gives bonus points.
- Background music — Session 8 covers this properly, but you can sneak-preview it.
- Particle effect on catch — small dots appear at the catch position for half a second.
- High score saved to file —
with open("highscore.txt", "w") as f: f.write(str(score)). Load on startup. Persistence callback to Phase 5 Session 6. - Two players — two baskets, two scores, two lives. WASD vs arrows.
- Falling enemies that target the basket — they steer toward you while falling.
What’s next
Next week is the big one: Pong, built together. Two paddles, one ball, a score. We’ll combine everything from Sessions 1-5 into one complete classic game. Bring your enthusiasm.
Session 6: Build Pong together
Session 6: Build Pong together
Phase 6 — Pygame · Session 6 of 14
What we’re learning today
Today we build Pong — the first commercial video game, released in 1972, and still a perfect example of game design. Two paddles, one ball, a score. We’ll build it together in six steps, each one running and playable before we move on. By the end of class, you’ll have a complete two-player game.
This is the first big build of Phase 6 — combining the frame loop, drawing, input, and collision into one working game.
You’ll need to remember from last time
- The frame loop (Session 1).
pygame.draw.rectandcircle(Session 2).- The Rect class (Session 3) —
x,y,width,height,center,move_ip,clamp_ip. get_pressedfor held keys (Session 4).rect.colliderect(Session 5).f"text {variable}"strings.
Part A: Build Pong, step by step
Open Thonny. Save a new file as pong.py.
We’ll build in six steps. Run after every step. Don’t skip — each step builds on the one before.
Step 1 — The window and the loop
Standard Pygame setup:
import pygame
pygame.init()
WIDTH, HEIGHT = 700, 500
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Pong")
clock = pygame.time.Clock()
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill(BLACK)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. A black window with the title “Pong.”
What’s slightly new:
WIDTH, HEIGHT = 700, 500— Python tuple unpacking on a single line. Two variables in one assignment.WHITE,BLACKconstants — uppercase signals “named value, won’t change.” Easier to read than raw RGB tuples scattered everywhere.
Step 2 — Two paddles
Two paddles, one on each side. Just rectangles drawn with
pygame.draw.rect.
Above the loop:
PADDLE_WIDTH = 12
PADDLE_HEIGHT = 80
left_paddle = pygame.Rect(40, HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT)
right_paddle = pygame.Rect(WIDTH - 40 - PADDLE_WIDTH, HEIGHT // 2 - PADDLE_HEIGHT // 2, PADDLE_WIDTH, PADDLE_HEIGHT)
In the draw section:
pygame.draw.rect(screen, WHITE, left_paddle)
pygame.draw.rect(screen, WHITE, right_paddle)
Save. Run. Two white paddles, one on each side, vertically centered.
What’s new:
pygame.Rect(x, y, w, h)— create a Rect directly, not from an image. We’ll use it for the paddle’s shape AND its collision box.HEIGHT // 2 - PADDLE_HEIGHT // 2— the math to center vertically.//is integer division (whole numbers). 500 // 2 = 250, 80 // 2 = 40, so paddle starts at y = 210 (center on the screen’s middle).
Step 3 — Move the paddles
Left paddle: W (up) and S (down). Right paddle: ↑ (up) and ↓ (down).
In the loop, after the event handling:
PADDLE_SPEED = 6
keys = pygame.key.get_pressed()
if keys[pygame.K_w] and left_paddle.top > 0:
left_paddle.y -= PADDLE_SPEED
if keys[pygame.K_s] and left_paddle.bottom < HEIGHT:
left_paddle.y += PADDLE_SPEED
if keys[pygame.K_UP] and right_paddle.top > 0:
right_paddle.y -= PADDLE_SPEED
if keys[pygame.K_DOWN] and right_paddle.bottom < HEIGHT:
right_paddle.y += PADDLE_SPEED
Save. Run. Hold W or S — left paddle moves. Hold ↑ or ↓ —
right paddle moves. Both stay on screen because of the
top > 0 and bottom < HEIGHT checks.
Step 4 — The ball
A bouncing ball — like Session 1, but for a circle.
Above the loop:
BALL_RADIUS = 10
ball_x = WIDTH // 2
ball_y = HEIGHT // 2
ball_dx = 5
ball_dy = 4
In update (after paddle movement):
ball_x += ball_dx
ball_y += ball_dy
# Bounce off top and bottom
if ball_y - BALL_RADIUS < 0 or ball_y + BALL_RADIUS > HEIGHT:
ball_dy = -ball_dy
In draw:
pygame.draw.circle(screen, WHITE, (ball_x, ball_y), BALL_RADIUS)
Save. Run. The ball flies across, bounces off the top and bottom, and disappears off the right edge. (We’ll deal with that in Step 6.)
Step 5 — Bounce off paddles
Now collision. The ball needs a Rect to collide with.
Replace your ball position with a Rect-based version:
Above the loop, change to:
BALL_SIZE = 20
ball_rect = pygame.Rect(0, 0, BALL_SIZE, BALL_SIZE)
ball_rect.center = (WIDTH // 2, HEIGHT // 2)
ball_dx = 5
ball_dy = 4
About those two lines: a Rect is created with its
top-left at (0, 0), but you usually want to position
it by its center (especially for a ball — “ball is in the
middle of the screen” makes more sense than “ball’s top-left
corner is somewhere”). So we make the rect with size and a
throwaway position, then immediately set .center to where
we actually want it. Rect has shortcuts for every edge —
.top, .bottom, .left, .right, .centerx,
.centery — and assigning to any of them moves the whole
rect to land that edge or center where you said. Useful
the rest of this session.
In update, replace the position math with rect math:
ball_rect.x += ball_dx
ball_rect.y += ball_dy
# Bounce off top and bottom
if ball_rect.top < 0 or ball_rect.bottom > HEIGHT:
ball_dy = -ball_dy
# Bounce off paddles
if ball_rect.colliderect(left_paddle) or ball_rect.colliderect(right_paddle):
ball_dx = -ball_dx
In draw, change the circle to a rect (or keep it as a circle drawn at the rect’s center — your choice):
pygame.draw.rect(screen, WHITE, ball_rect)
# OR
# pygame.draw.circle(screen, WHITE, ball_rect.center, BALL_SIZE // 2)
Save. Run. The ball now bounces off the paddles! Move the paddles to intercept. This is starting to feel like a real game.
There’s a subtle bug here that real Pong programs deal with: if the ball is moving fast and the paddle is positioned just so, the ball can get stuck inside the paddle (the bounce flips direction every frame, and the ball can’t escape). We’ll ignore this for now — it’s rare at low speeds. Real Pong does extra logic to push the ball back out of the paddle when this happens.
Step 6 — Score
When the ball goes off the left edge, right player scores. When it goes off the right, left player scores. Then reset the ball to the middle.
Above the loop:
left_score = 0
right_score = 0
font = pygame.font.SysFont("Arial", 60)
def reset_ball():
ball_rect.center = (WIDTH // 2, HEIGHT // 2)
In update, after the paddle bounce check:
# Score: ball off the left or right edge
if ball_rect.right < 0:
right_score += 1
reset_ball()
ball_dx = abs(ball_dx) # send it right (toward the loser)
if ball_rect.left > WIDTH:
left_score += 1
reset_ball()
ball_dx = -abs(ball_dx)
In draw, after the paddles and ball:
left_text = font.render(str(left_score), True, WHITE)
right_text = font.render(str(right_score), True, WHITE)
screen.blit(left_text, (WIDTH // 4, 20))
screen.blit(right_text, (WIDTH * 3 // 4 - right_text.get_width(), 20))
Save. Run. You have Pong. Score appears at the top. Ball resets when someone scores. Two players can play against each other.
A few details:
abs(ball_dx)= absolute value (always positive). After resetting, send the ball toward the loser (positive dx = right, negative = left).- Centered scores with two different alignment tricks — left score positioned by left edge, right score positioned by right edge.
Checkpoint: You have a working two-player Pong game with paddles, a bouncing ball, and a score. This is the natural stop point if class is cut short. (Today’s goal.)
Part B: Make Pong yours
Time to add polish. Pick any of these — or invent your own.
Stretch — speed up over time
Each paddle bounce, slightly increase ball speed:
if ball_rect.colliderect(left_paddle) or ball_rect.colliderect(right_paddle):
ball_dx = -ball_dx
# Speed up
if ball_dx > 0:
ball_dx += 0.5
else:
ball_dx -= 0.5
if ball_dy > 0:
ball_dy += 0.3
else:
ball_dy -= 0.3
(Note: with floats now, ball_rect.x will quietly round.
Fine for this game.)
Reset speed in reset_ball() so a new round starts at
normal speed.
Stretch — ball direction depends on where it hit
In real Pong, hitting the ball with the top of the paddle sends it up; the bottom sends it down. Better gameplay:
if ball_rect.colliderect(left_paddle):
ball_dx = abs(ball_dx) # always go right after left paddle
# vertical based on where it hit
relative = (ball_rect.centery - left_paddle.centery) / (left_paddle.height / 2)
ball_dy = relative * 8
if ball_rect.colliderect(right_paddle):
ball_dx = -abs(ball_dx)
relative = (ball_rect.centery - right_paddle.centery) / (right_paddle.height / 2)
ball_dy = relative * 8
The math: relative is between -1 (top of paddle) and +1
(bottom). Multiplied by 8 = ball y-speed between -8 and
+8.
Stretch — center line
A dashed line down the middle (cosmetic):
for y in range(0, HEIGHT, 30):
pygame.draw.rect(screen, WHITE, (WIDTH // 2 - 2, y, 4, 15))
Add to draw section. Looks like real Pong.
Stretch — winning the game
Game ends at 10 points. Show a win screen.
Above the loop:
WIN_SCORE = 10
game_over = False
winner = ""
After scoring:
if left_score >= WIN_SCORE:
game_over = True
winner = "Left"
if right_score >= WIN_SCORE:
game_over = True
winner = "Right"
Wrap movement and ball update in if not game_over:.
In draw, after everything else:
if game_over:
big_font = pygame.font.SysFont("Arial", 50)
text = big_font.render(f"{winner} wins! Press R to restart", True, WHITE)
rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
screen.blit(text, rect)
Add R-to-restart in the event handling.
Extension — single player vs computer
Replace the right paddle with AI: each frame, move the right paddle toward the ball.
# Replace the right_paddle keyboard checks with:
if right_paddle.centery < ball_rect.centery and right_paddle.bottom < HEIGHT:
right_paddle.y += PADDLE_SPEED - 2 # AI is slightly slower
if right_paddle.centery > ball_rect.centery and right_paddle.top > 0:
right_paddle.y -= PADDLE_SPEED - 2
Now it’s one player vs the computer. The “minus 2” makes the AI beatable; without it, the AI is perfect.
Wrap-up
Before we leave, share with the room:
- For everyone — show your Pong. Did you and your buddy play a round?
- For the kids who added the angle-by-position bounce — does it feel more like real Pong?
- For the kids who built single-player AI — can you beat it?
- What’s the highest score you saw?
Today you built a complete classic game from scratch. Pong is a perfect example because it’s minimal — six sessions of skills add up to a real, playable, fun game.
You used everything from Sessions 1-5:
- The frame loop (Session 1)
- Drawing rectangles + the screen size (Session 2)
- The Rect class (Session 3)
- Polled keyboard input (Session 4)
colliderect(Session 5)
Plus one new piece — pygame.Rect(x, y, w, h) to make a
Rect directly, without an image.
Next week we put your Pong on the internet — push it to GitHub, where the world can see it. After that: sound. Then sprite classes (so we can refactor games with many objects). Then Mr. Eric’s grid-world.
If you missed this session
Open Thonny. Then:
-
Build Pong step by step. Don’t skip steps. Run after each.
-
By the end you should have two paddles, a bouncing ball, and a score.
-
(Stretch) Add at least one polish item — speed-up, angle-by-position, center line, or win screen.
About 60-90 minutes total — this is a long session.
Stretch and extension ideas
- All the stretches above — speed-up, angle bounce, center line, win screen, AI opponent.
- Sound effects — Session 8 will cover the proper
way. For now, you can sneak-preview:
bounce_sound = pygame.mixer.Sound("bounce.wav") # then bounce_sound.play() on collision - Multi-color ball — change ball color each bounce.
- Trail effect — instead of
screen.fill(BLACK), fill with a translucent black usingpygame.Surfacealpha. Ball leaves a fading trail. - Powerups — random items appear on the field. Catch with the ball: bigger paddle, faster ball, freeze opponent.
- Touch-up the visuals — fancier scoreboard, chamfered paddles (rounded corners), pixel-art ball.
- Save your high score to a file (Phase 5 callback).
What’s next
Next week: GitHub. You’ll create a GitHub account, push your Pong to the internet, and have a real public repo with your name on it. By the end of class, you can share a link to your game with anyone.
Session 7: GitHub — push to the world
Session 7: GitHub — push to the world
Phase 6 — Pygame · Session 7 of 14
What we’re learning today
Up to now, your code has lived only on your machine. If your hard drive died tomorrow, all your projects would be gone. Today we put your code on GitHub — a free service that stores Git repositories in the cloud and shares them with anyone you want. By the end of class, your Pong from last week will be on the internet, with your name on it, at a real URL anyone can visit.
This is a foundational moment. Every working programmer in the world uses GitHub (or something like it). Today you join them.
You’ll need to remember from last time
- Your Pong project from Session 6 — the folder.
- Git from Phase 4 Sessions 5-6 —
init,add,commit,log,status. - The terminal from Phase 4 Session 1 —
cd,ls.
What you’ll need today
- A GitHub account. If you don’t have one yet, we’ll create one in class. (You’ll need an email address.)
- Your Pong project folder, with a Git history (you’ve been committing as you go since Session 6, right?).
Part A: Your first push to GitHub
What is GitHub?
A Git repository lives on your machine. So far, all your repos do. But Git is built to be distributed — copies of the same repo can live on multiple machines, and you can sync them.
GitHub is a website where Git repos live in the cloud. Other people (or future-you on a different machine) can grab a copy. You can push your changes up; others can pull them down. It’s a shared whiteboard for code.
A few things GitHub adds on top of plain Git:
- A web view of your code — anyone can browse it in a browser.
- Issues — a bug tracker.
- Pull requests — a way to suggest changes to someone else’s code.
- Profiles — your
github.com/<username>page is a real portfolio.
For now, we just want storage and sharing. The other features will come up over time.
Step 1 — Make a GitHub account
If you already have one, skip this.
- Go to github.com.
- Click Sign up.
- Pick a username — this becomes part of your URLs and profile, so pick something you’d be okay with for years. (Real name is fine. Nickname is fine. Avoid anything you’d regret.)
- Use an email address you actually check. GitHub will send a verification email.
- Pick a strong password. Save it somewhere safe.
- Verify your email (click the link in the email GitHub sent).
Tell Mr. Eric your username when you’re done.
Step 2 — Create an empty repo on GitHub
- After logging in, click the + in the top-right, then New repository.
- Repository name:
pong. (Or whatever you called your project.) - Description (optional): “My first Pygame project.”
- Public. (Private hides it from everyone. Public means anyone can see it. For a personal project, public is fine.)
- Do NOT check “Add a README file” or any other options. We want a completely empty repo.
- Click Create repository.
GitHub shows you a page with setup instructions. Don’t follow those yet — they’re for fresh projects, but ours already has commits. We’ll use slightly different commands.
Step 3 — Connect your local repo to GitHub
Open a terminal. cd into your Pong project folder:
$ cd ~/projects/pong # adjust to wherever yours is
$ git status # confirm you're in the right place
Now connect this local repo to the empty GitHub repo:
$ git remote add origin https://github.com/YOUR-USERNAME/pong.git
(Replace YOUR-USERNAME with your actual GitHub
username. You can also copy this exact URL from the
GitHub setup page.)
What this does:
git remote add— register a remote (a Git repo somewhere else).origin— the conventional name for “the main remote.” You’ll see this everywhere in Git.- The URL — where the remote lives.
Confirm it worked:
$ git remote -v
Should show two lines (one for fetch, one for push) both pointing at your GitHub URL.
Step 4 — Push
$ git push -u origin main
What this does:
git push— send commits to a remote.-u origin main— “send mymainbranch to theoriginremote, and remember that connection.” (The-uis short for--set-upstream. Future pushes can just begit push— no arguments needed.)
You’ll be asked for your GitHub username and a password.
For the password, don’t use your account password — GitHub stopped accepting that for command-line use in 2021. Instead, use a Personal Access Token (PAT). Mr. Eric will help you create one if you haven’t already.
(See the Personal Access Token setup section below for the full steps.)
After you enter username and PAT, Git pushes your commits. You’ll see something like:
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
...
To https://github.com/YOUR-USERNAME/pong.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
That last line is the win.
Step 5 — Look at it on GitHub
Refresh the GitHub page in your browser. Your code is
there. Click around. Click on pong.py to see the
code. Click on README.md if you have one (or skip it
if you don’t).
The URL is something like
https://github.com/YOUR-USERNAME/pong. You can share
that with anyone in the world. They can read your code
in their browser.
Checkpoint: Your Pong is on GitHub. You can see it
in your browser at your github.com/YOUR-USERNAME/pong
URL. This is the natural stop point if class is cut
short.
Part B: Make it look nice (and update it)
Time to add polish. A bare folder of code is fine — but a good README turns it into a real project.
Add a README
If you don’t already have one, create a README.md in
your project folder:
# Pong
My first Pygame project — a recreation of the 1972
classic, two-player Pong.
## How to play
- Left paddle: **W** (up) and **S** (down)
- Right paddle: **↑** (up) and **↓** (down)
- First to 10 wins.
## How to run
You need Python 3 and Pygame.
pip install pygame python pong.py
## Built with
- Python
- Pygame
- A lot of patience
Save. The triple-backticks (```) make code blocks in
Markdown. The # and ## make headings.
Commit and push the README
$ git status # see the new README
$ git add README.md
$ git commit -m "Add README with controls and run instructions"
$ git push
Notice the second push — no -u origin main needed
anymore. Git remembers from before.
Refresh GitHub. Your README shows up on the repo page — automatically rendered with proper formatting. Nice.
Stretch — add a screenshot
A picture sells your project. Take a screenshot of Pong
running. Save it as screenshot.png in the project
folder. Add to your README:
## Screenshot

The  syntax embeds an image in
Markdown.
$ git add screenshot.png README.md
$ git commit -m "Add screenshot to README"
$ git push
Refresh GitHub. The image appears.
Stretch — push more projects
Pick another project — your fruit catcher, your collector, any of your milestones. Same process:
- Make a new repo on GitHub (empty).
- In the project folder,
git remote add origin <URL>. git push -u origin main.
Each project becomes its own repo on your profile. After
a few of these, your github.com/YOUR-USERNAME page is
a portfolio.
Stretch — explore other people’s repos
Browse to github.com/python to see Python’s source.
Or github.com/pygame/pygame to see Pygame’s. Real
production code, written by professionals, all visible.
This is one of the best things about software — most of
it is open and you can read it.
(Don’t copy without permission — open code has licenses that say what you can and can’t do. But reading is always allowed.)
Extension — clone a repo
git clone <URL> makes a local copy of any GitHub repo.
Try cloning a small Pygame example from
github.com/pygame/pygame (look in the examples/
folder).
$ git clone https://github.com/pygame/pygame.git
Now you have all of Pygame’s source code in a folder. You can read it, run examples, even modify your local copy.
Personal Access Token setup
GitHub uses Personal Access Tokens (PATs) instead of passwords for command-line access. To create one:
- On GitHub, click your profile picture → Settings.
- In the left sidebar, scroll down to Developer settings.
- Personal access tokens → Tokens (classic) → Generate new token (classic).
- Note: “Class machine” or similar.
- Expiration: No expiration is easiest for a class machine, but 90 days is more secure.
- Scopes: Check repo (the top-level box — selects all repo permissions).
- Click Generate token.
- COPY THE TOKEN somewhere safe. You won’t see it again after leaving this page.
When git push asks for a password, paste the token.
If your machine is configured to remember credentials, you’ll only need to do this once. If not, every push will ask.
(Modern alternative: SSH keys. More setup, no passwords ever after that. We can cover this if there’s interest.)
Wrap-up
Before we leave, share with the room:
- For everyone — what’s your GitHub URL? Show your repo.
- Is your README up?
- Did you push more than one project?
- Anyone explore another GitHub repo? See anything cool?
Today you went from “my code is on my machine” to “my code is on the internet, with my name on it.” That’s a real shift. You can now:
- Share a working project as a single URL.
- Show your portfolio to anyone (parents, friends, teachers, future employers).
- Recover your code from any machine —
git cloneyour own repo. - Collaborate with others (we’ll touch this in later phases).
This is real software-engineering infrastructure. Used correctly, it lasts your whole career.
You learned today:
- GitHub is a free Git hosting service.
git remote add origin <URL>— connect a local repo to a remote.git push -u origin main— send commits to the remote (the first time).git push— same thing, after the first time.- Personal Access Tokens for password-replacement.
- Markdown READMEs make repos look real.
- Public repos = portfolio.
If you missed this session
You’ll need a GitHub account and a project to push.
-
Create a GitHub account at github.com if you don’t have one.
-
Make a Personal Access Token (steps in this handout, under “Personal Access Token setup”).
-
In your project folder:
git remote add origin https://github.com/USERNAME/REPO.git git push -u origin main -
Add a README with what your project does and how to run it.
-
Push the README.
About 30-45 minutes the first time. Once your Git + GitHub setup works, future pushes are instant.
Stretch and extension ideas
- Push every project you’ve built so far — Phase 1 through Phase 6. Each becomes a repo. Your profile fills up.
- Customize your profile. Settings → Profile. Add a photo, bio, website.
- Star repos you like. Click the star button on any repo to bookmark it.
- Follow people whose work you like. Pygame developers, classmates, anyone.
- Try GitHub Pages — a feature that turns any HTML in your repo into a public website. We’ll use this more in Phase 7.
- SSH key setup — replace passwords with key-based auth. Faster, more secure. Ask Mr. Eric if you’re interested.
.gitignore— a file that tells Git which files to not track (like__pycache__/,.DS_Store,*.pyc). Add one to your projects:__pycache__/ *.pyc .DS_Store
What’s next
Next week: sound and music. Pygame can play sound effects and background music. We’ll add bounce and score sounds to Pong, then experiment with music. Your games go from silent to alive.
Session 8: Sound and music
Session 8: Sound and music
Phase 6 — Pygame · Session 8 of 14
What we’re learning today
Your games have been silent. Today changes that.
Pygame’s mixer plays sound effects (short clips like
a “bounce” or a “ding”) and music (longer tracks that
loop in the background). By the end of class, your Pong
will sound like a game — paddle bounces, scoring, and
maybe even background music.
Sound is the smallest amount of code with the biggest “feel” change in any game.
You’ll need to remember from last time
- Your Pong project from Session 6 (or any other game).
- Loading resources outside the loop — Session 3. Same idea applies to sounds.
pygame.event.KEYDOWN— Session 4. Sounds usually trigger from events.
What you’ll need today
- Some sound files. In class, there’s a folder of free game sounds for you. At home, you can grab free sounds from freesound.org or opengameart.org.
- A working game (Pong, fruit catcher, collector — any one works).
Part A: Sound effects
Two kinds of audio
Pygame splits audio into two APIs:
pygame.mixer.Sound— short clips (a beep, a bounce, a click). Can play many at once. Loaded fully into memory.pygame.mixer.music— long tracks (background music, a song). Streamed from disk, only one at a time.
Use Sound for game effects. Use music for background tracks.
Loading and playing a sound
The basic pattern. Open a copy of your Pong (or any game). Add at the top:
pygame.init()
# ... existing setup ...
# Load sounds — once, outside the loop
bounce_sound = pygame.mixer.Sound("bounce.wav")
score_sound = pygame.mixer.Sound("score.wav")
Inside the loop, where the relevant thing happens:
if ball_rect.colliderect(left_paddle):
ball_dx = -ball_dx
bounce_sound.play() # ← here
if ball_rect.right < 0:
right_score += 1
reset_ball()
score_sound.play() # ← here
Save. Run. You should hear a sound when the ball bounces. And a different one when someone scores.
That’s it. Two lines per sound.
Sound formats
Pygame supports .wav, .ogg, and .mp3 (with the
right system libraries — sometimes spotty on Linux).
- WAV — uncompressed. Big files, instant load, perfect quality. Best for sound effects.
- OGG — compressed, open format. Smaller files, good quality. Best for music.
- MP3 — compressed, common. Sometimes works, sometimes doesn’t. Convert to OGG if you have issues.
For sound effects (short clips), stick to WAV.
Volume
Sounds can be quieter. Set volume from 0.0 (silent)
to 1.0 (full):
bounce_sound.set_volume(0.4)
Per-sound, set once after loading. Or change at runtime to make a sound fade.
You can also set the channel’s volume — but for most games, per-sound is enough.
Sound + boundaries
Add a “wall bounce” sound for when the ball hits the top or bottom:
wall_sound = pygame.mixer.Sound("wall.wav")
# in update:
if ball_rect.top < 0 or ball_rect.bottom > HEIGHT:
ball_dy = -ball_dy
wall_sound.play()
The two-tone “bonk-bing” of a Pong bounce comes from having different sounds for paddle vs wall hits.
Checkpoint: Your game plays sounds when at least two different things happen. This is the natural stop point if class is cut short.
Part B: Background music
Now the long-form audio. Music plays continuously while the game runs.
Load and play
pygame.mixer.music.load("background.ogg")
pygame.mixer.music.set_volume(0.3) # quieter than effects
pygame.mixer.music.play(-1) # -1 means loop forever
The play(-1) is the key — it loops the track
indefinitely. play(1) plays it once. play(3) plays
it three times, then stops.
Add this once, near the start of your program (after
pygame.init()). The music starts playing immediately.
Run. Background music plays. Sound effects (paddle bounces, scores) play over the music. Both work.
Pausing and stopping music
Common controls:
pygame.mixer.music.pause() # pause
pygame.mixer.music.unpause() # resume
pygame.mixer.music.stop() # stop completely
pygame.mixer.music.set_volume(0.0) # silent (without stopping)
Often games have a key to mute. In your event handling:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_m:
# toggle music
if pygame.mixer.music.get_busy():
pygame.mixer.music.pause()
else:
pygame.mixer.music.unpause()
Press M to toggle music.
Stretch — switch tracks for game over
When the game ends, switch from upbeat music to something more dramatic. You’ll need a second track:
# at game over:
pygame.mixer.music.load("game_over.ogg")
pygame.mixer.music.play() # play once
You can load a new track at any time.
Stretch — sound channels
For more advanced control, Pygame uses channels. By default sounds play on whichever channel is free; you can also pin a sound to a specific channel:
channel = pygame.mixer.Channel(0)
channel.play(bounce_sound)
channel.set_volume(0.5)
Useful for “I want this kind of sound to never overlap itself” or “I want music in one channel and effects in another.” Don’t drill — mention only if asked.
Extension — record your own sound
Free apps like Audacity (or any phone voice recorder) let you record short clips. Save as WAV. Use as game sounds. Your game now has your voice or your tap in it.
If you have a buddy nearby, record them saying “score!” or “ouch!” and use it. Suddenly it’s your game.
Extension — sound for every event
Add a sound for everything:
- Game start: an intro sound.
- Each score: a different ding.
- Bouncing off a wall vs paddle: two sounds.
- Each paddle: different sounds (left vs right).
- A “warning” sound when one player is one point from losing.
Each one is two lines of code (load + play). Add up to a very polished feel.
Wrap-up
Before we leave, share with the room:
- For everyone — show your game with sound. Did the feel change?
- For the kids who added music — what music?
- Anyone find or record a particularly satisfying sound?
- (Honest question:) Is your game more fun with sound? Or just louder?
Today you learned:
- Two audio APIs:
mixer.Sound(effects) andmixer.music(music). - Load once, play many times — same pattern as images.
.set_volume(0.0 - 1.0)for individual sounds..play(-1)for looping background music.- Sound triggers from events or game state — bounce, score, lose.
Sound is the smallest amount of code with the biggest feel change in any game. Two lines per sound. Huge difference.
Next up: sprite classes and groups — using Phase 4’s
classes properly. Your games have many objects — bullets,
enemies, particles. Today you’d track each in a list.
Next session, sprites become real Sprite instances,
and Pygame’s Group handles them all at once.
If you missed this session
Open Thonny. Get a sound effect (any short WAV) and optionally a music file (any OGG).
-
Open one of your games (Pong, fruit catcher, etc.).
-
Load the sound:
bounce_sound = pygame.mixer.Sound("bounce.wav"). -
Play it where appropriate (collision, score, etc.):
bounce_sound.play(). -
(Stretch) Add background music:
pygame.mixer.music.load("background.ogg") pygame.mixer.music.play(-1) -
(Stretch) Add a mute key (M).
About 30-45 minutes. By the end your game should make noise.
Stretch and extension ideas
- Different sound per event — wall bounce vs paddle bounce vs score vs win.
- Volume control — keys to turn music up/down.
- Mute toggle.
- Music switches for different game states (intro, playing, game over).
- Random sound variety — instead of one bounce
sound, pick from a list of three each time.
bounce_sounds = [ pygame.mixer.Sound("bounce1.wav"), pygame.mixer.Sound("bounce2.wav"), pygame.mixer.Sound("bounce3.wav"), ] random.choice(bounce_sounds).play() - Recorded sounds — your voice, your tap, your whatever. WAV files work fine.
- Sound chiptune music — search “chiptune music free” for retro game-style tracks.
- Push the new sound version to GitHub (Session 7
callback).
git add,commit -m "Add sound effects",git push.
What’s next
Next week: sprite classes and groups. Your games
have lots of objects. Tracking them in raw lists
(fruits = [], enemies = []) works for a while but
gets messy fast. Pygame’s Sprite class and Group
container make organizing many objects much cleaner.
This is where Phase 4’s classes really pay off.
Session 9: Sprite classes and groups
Session 9: Sprite classes and groups
Phase 6 — Pygame · Session 9 of 14
What we’re learning today
Your games have a lot of objects. Each fruit was a [rect, speed] pair in a list. Each enemy too. As games get
bigger, this gets messy fast. Today we use Pygame’s
Sprite class — one Python class per kind of
object — and Group containers that update and draw
many sprites at once. By the end you’ll have refactored
the fruit catcher into a much cleaner shape, and you’ll
have the production pattern that real Pygame projects
use.
This is where Phase 4’s classes really pay off.
You’ll need to remember from last time
- Classes — Phase 4 Session 4.
class Pet:,__init__,self, methods. - The fruit catcher from Session 5 — basket and a list of falling fruit.
- Pygame Rect (Session 3) —
image,rect,blit. colliderect(Session 5).- Frame loop — input, update, draw.
Part A: Your first Sprite
What’s a Sprite?
Pygame’s pygame.sprite.Sprite is a base class. You
create your own classes that inherit from it. Each
sprite holds:
- An
image(its picture, usually aSurface). - A
rect(its position). - An
update()method (what it does each frame).
Then pygame.sprite.Group is a container. You stuff
sprites into it. The group has:
group.update()— callsupdate()on every sprite.group.draw(screen)— draws every sprite to the screen.pygame.sprite.spritecollide(sprite, group, dokill)— finds collisions between a sprite and a group.
The win: instead of looping over a list and calling update + draw + collision check yourself, the group does it all.
Build a Player class
Open Thonny. Save a new file as sprite_basics.py. Type:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("Sprite class")
clock = pygame.time.Clock()
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.image.load("player.png")
self.rect = self.image.get_rect(center=(x, y))
self.speed = 5
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.rect.x += self.speed
if keys[pygame.K_UP] or keys[pygame.K_w]:
self.rect.y -= self.speed
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
self.rect.y += self.speed
self.rect.clamp_ip(screen.get_rect())
# Create one player and put it in a group
all_sprites = pygame.sprite.Group()
player = Player(300, 200)
all_sprites.add(player)
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
all_sprites.update()
screen.fill((100, 150, 100))
all_sprites.draw(screen)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. WASD or arrows to move the player.
What’s new:
class Player(pygame.sprite.Sprite):— Player inherits from Sprite. (Phase 4 Session 4 didn’t cover inheritance — this is a small new step. The parentheses say “based on this other class.”)super().__init__()— call the parent class’s init. Required when you inherit. Phase 4 didn’t show this; it’s a one-liner you always add.self.imageandself.rect— the two attributes Pygame’s Group expects. Names matter — Group looks for these specifically.update(self)method — Pygame Group calls this on every sprite each frame.pygame.sprite.Group()— make an empty group.all_sprites.add(player)— add a sprite to the group.all_sprites.update()— calls each sprite’supdate().all_sprites.draw(screen)— blits each sprite.
Look at the loop. Three lines of game logic: event, update, draw. The Player class holds all the details.
This is the production pattern.
Add a Coin class
class Coin(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.image.load("coin.png")
self.rect = self.image.get_rect(center=(x, y))
def update(self):
pass # coins don't move
Then create a few:
import random
coins = pygame.sprite.Group()
for _ in range(8):
cx = random.randint(50, 550)
cy = random.randint(50, 350)
coin = Coin(cx, cy)
coins.add(coin)
all_sprites.add(coin) # also add to all_sprites for drawing
Two groups: coins (just the coins, for collision
checking) and all_sprites (everything, for drawing).
In the draw section, all_sprites.draw(screen) already
draws everything. No change needed there.
Group-based collision
Replace your old loop-based collision with:
hit_coins = pygame.sprite.spritecollide(player, coins, True)
score += len(hit_coins)
spritecollide(player, coins, True):
- Check
playeragainst every sprite incoins. - Returns a list of the coin sprites that the player is touching.
- The
Truemeans “kill collided sprites” — they’re removed from all their groups automatically.
Add a score variable above the loop, and a font/text
to display it (Session 5 pattern).
Save. Run. Walk over the coins. They disappear. Score goes up. Same game as Session 5, much less code.
Checkpoint: You have a sprite-class-based game with a Player, multiple Coins, and group collision. This is the natural stop point if class is cut short.
Part B: Refactor the fruit catcher
The Session 5 fruit catcher used lists of [rect, speed]
pairs. Today we re-do it with proper sprite classes.
Build it
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((600, 500))
pygame.display.set_caption("Fruit catcher (sprites)")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 28)
class Basket(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.image.load("basket.png")
self.rect = self.image.get_rect(midbottom=(300, 490))
self.speed = 7
def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
self.rect.x += self.speed
self.rect.clamp_ip(screen.get_rect())
class Fruit(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.image.load("fruit.png")
x = random.randint(20, 580)
self.rect = self.image.get_rect(center=(x, -20))
self.speed = random.randint(3, 6)
def update(self):
self.rect.y += self.speed
if self.rect.top > 500:
self.kill() # remove from all groups
global lives
lives -= 1
# Setup
all_sprites = pygame.sprite.Group()
fruits = pygame.sprite.Group()
basket = Basket()
all_sprites.add(basket)
score = 0
lives = 3
spawn_timer = 0
SPAWN_INTERVAL = 60
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if lives > 0:
all_sprites.update()
# Catch — collision check
caught = pygame.sprite.spritecollide(basket, fruits, True)
score += len(caught)
# Spawn
spawn_timer += 1
if spawn_timer >= SPAWN_INTERVAL:
new_fruit = Fruit()
all_sprites.add(new_fruit)
fruits.add(new_fruit)
spawn_timer = 0
# Draw
screen.fill((180, 220, 255))
all_sprites.draw(screen)
score_text = font.render(f"Score: {score}", True, (0, 0, 0))
lives_text = font.render(f"Lives: {lives}", True, (180, 0, 0))
screen.blit(score_text, (10, 10))
screen.blit(lives_text, (480, 10))
if lives <= 0:
over_text = font.render("GAME OVER", True, (200, 0, 0))
rect = over_text.get_rect(center=(300, 250))
screen.blit(over_text, rect)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. Same fruit catcher as Session 5 — same gameplay, different shape inside.
What changed:
- The
Basketclass holds both its image AND its movement logic. - The
Fruitclass holds its image AND its falling logic AND its “I went off-screen” handling. - The main loop is much smaller. Three lines of update logic, plus spawn and collision. Compare to Session 5.
self.kill()— a Sprite removes itself from all its groups. No morecoins.remove(coin_rect).global livesinFruit.update()— the sprite modifies the outerlivesvariable. Not pretty, but works. (Real games would pass a game-state object to the sprite. We’ll skip that.)
The biggest win: adding a new fruit type is now a new class. No special-case handling in the main loop.
Stretch — add bombs as a subclass
class Bomb(Fruit):
def __init__(self):
super().__init__()
self.image = pygame.image.load("bomb.png")
# rect is already set by parent's __init__
def update(self):
self.rect.y += self.speed
if self.rect.top > 500:
self.kill() # bombs that fall through don't lose lives
# In spawn:
if random.random() < 0.2: # 20% chance
new_thing = Bomb()
else:
new_thing = Fruit()
all_sprites.add(new_thing)
fruits.add(new_thing)
Bombs inherit from Fruit. Same falling behavior, but different image and different “fall through” handling (no life lost — bombs are good to miss).
For the catch logic, bombs should lose a life when caught. Update collision:
caught = pygame.sprite.spritecollide(basket, fruits, True)
for thing in caught:
if isinstance(thing, Bomb):
lives -= 1
else:
score += 1
isinstance(thing, Bomb) checks the type. Bombs lose a
life on catch; fruits add to score on catch.
Stretch — sprite that moves on its own
Add a class for an enemy that bounces around the screen on its own:
class Enemy(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.image.load("enemy.png")
self.rect = self.image.get_rect(center=(100, 100))
self.dx = random.choice([-3, 3])
self.dy = random.choice([-3, 3])
def update(self):
self.rect.x += self.dx
self.rect.y += self.dy
if self.rect.left < 0 or self.rect.right > 600:
self.dx = -self.dx
if self.rect.top < 0 or self.rect.bottom > 500:
self.dy = -self.dy
Add some to the game. The basket should avoid them.
Extension — refactor your Pong
Pong has three sprites — two paddles, one ball. Refactor it:
class Paddle(pygame.sprite.Sprite):
def __init__(self, x, up_key, down_key):
super().__init__()
self.image = pygame.Surface((12, 80))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect(center=(x, 250))
self.up_key = up_key
self.down_key = down_key
self.speed = 6
def update(self):
keys = pygame.key.get_pressed()
if keys[self.up_key] and self.rect.top > 0:
self.rect.y -= self.speed
if keys[self.down_key] and self.rect.bottom < 500:
self.rect.y += self.speed
class Ball(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((20, 20))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect(center=(350, 250))
self.dx = 5
self.dy = 4
def update(self):
self.rect.x += self.dx
self.rect.y += self.dy
if self.rect.top < 0 or self.rect.bottom > 500:
self.dy = -self.dy
Notice: pygame.Surface((12, 80)) — make a Surface of
that size (no image file needed). .fill((255,255,255))
paints it. Drawing-from-shapes built into the sprite.
The two paddles only differ by their position and which keys they use — passed as constructor arguments. One class, two instances. That’s the win.
Wrap-up
Before we leave, share with the room:
- For everyone — show your refactored fruit catcher. Does the code feel cleaner?
- For kids who added Bomb — does inheritance feel useful?
- For kids who refactored Pong — same gameplay, same result?
- Does the main game loop feel smaller now?
Today you learned:
pygame.sprite.Sprite— the base class for every game object.pygame.sprite.Group— container for many sprites.group.update()andgroup.draw(screen)— one call updates/draws all sprites.pygame.sprite.spritecollide(sprite, group, kill)— collision against a group.self.kill()— sprite removes itself.- Inheritance —
class Bomb(Fruit):makes Bomb a kind of Fruit with overrides. pygame.Surface(size)— a blank surface for drawing on (no image file needed).
The main loop shrinks when you use sprite classes. That’s not just style — it’s how production game code stays manageable as games grow. Tomorrow’s bigger games will use this pattern by default.
Next week: the grid-world. Mr. Eric’s coding-puzzle game — built on Pygame, controlled by Python code you write. You’ll play it as a user first. Week after, you’ll extend it.
If you missed this session
Open Thonny. Then:
-
Build the basic Player + Coins example from Part A. Walk over the coins. Watch them disappear.
-
Refactor your fruit catcher with sprite classes (Part B). Same gameplay, cleaner code.
-
(Stretch) Add a Bomb class that inherits from Fruit.
About 60-90 minutes — this is a meaty session.
Stretch and extension ideas
- Refactor Pong with sprite classes (above).
- Bombs in fruit catcher (above).
- Bouncing enemies that the basket must avoid.
- Multiple fruit types — Cherry, Apple, Banana — each worth different points.
- Sprite groups for visual layering — a
backgroundgroup drawn first, thenmid, thentop. Order matters. - A
Gameclass that holds everything. All your sprites and state become attributes of one Game instance. pygame.sprite.groupcollide(group_a, group_b, kill_a, kill_b)— two groups vs each other. Useful for “all bullets vs all enemies.”
What’s next
Next week: the grid-world. Mr. Eric built a coding-puzzle game where you control a character on a grid by writing Python. You’ll play through some puzzles and see how the game works. The week after, you’ll extend it — add new puzzles, new abilities, make it yours.
Session 10: The grid-world (intro)
Session 10: The grid-world (intro)
Phase 6 — Pygame · Session 10 of 14
What we’re learning today
Today is a little different. Mr. Eric built a game called grid-world, where you guide a character around a grid to reach a goal — by writing Python code. No keyboard controls; the character moves based on the code you write. By the end of class you’ll have solved several puzzles and you’ll understand how the program works inside (which sets you up to extend it next week).
Some online learn-to-code sites work like this — drag blocks to move a robot, solve a puzzle. You’re the ones building the toy instead of using someone else’s.
You’ll need to remember from last time
- Sprite classes — Session 9.
- Functions — Phase 3 Session 6.
- Loops —
for i in range(N):. - Conditionals —
if,else.
What you’ll need today
- The
grid_world.pyfile Mr. Eric wrote — it’ll be on your machine before class.
Part A: Play the puzzles
What is grid-world?
Open grid_world.py in Thonny.
The game shows an 8x8 grid. A character (a small blue square) starts in one corner. A goal (a yellow square) is somewhere else. There may be walls in between. Your job: write Python code that gets the character to the goal.
Run the program. Press 1, 2, 3, 4, 5 to switch between the five puzzles. Press SPACE to run your code. Press R to reset the character.
Look at the bottom of the file. You’ll see something like:
def solve():
# === YOUR CODE GOES HERE ===
pass
That solve() function is what gets run when you
press SPACE. Replace pass with calls to the movement
functions.
The movement functions
Mr. Eric defined a few functions you can call:
move_right() # one cell to the right
move_left() # one cell to the left
move_down() # one cell down
move_up() # one cell up
Each one moves the character one cell. You see the character animate from its current cell to the next.
Puzzle 1 — straight line
Press 1 to load Puzzle 1. The character is at the top-left, the goal is three cells to the right. No walls.
Edit solve():
def solve():
move_right()
move_right()
move_right()
Save. Press SPACE. The character walks to the goal. Solved.
Puzzle 2 — turn the corner
Press 2. Now the goal is down and to the right. You need to move right several times, then down.
def solve():
move_right()
move_right()
move_right()
move_right()
move_down()
move_down()
Save, press SPACE, watch the character solve it.
Puzzle 3 — use a loop
Press 3. The path is long — maybe 7 cells right.
Typing move_right() seven times is dumb. Use a for
loop:
def solve():
for _ in range(7):
move_right()
(The underscore _ means “I’m not using the loop
variable.” Convention.)
Save. Press SPACE. Same result, less code. That’s why we have loops.
Puzzle 4 — walls!
Press 4. There are walls between you and the goal. Walking into a wall doesn’t move the character (you’ll see it bump). You have to go around.
Plan the path on paper if it helps. Write the moves:
def solve():
move_right()
move_right()
move_down()
move_down()
move_right()
move_right()
move_down()
(Yours might be different depending on where the walls are.)
Puzzle 5 — long path
Press 5. The grid has a snaking path. Walk it.
Try writing the solution with a mix of loops and straight calls.
Checkpoint: You’ve solved at least three puzzles. This is the natural stop point if class is cut short.
Part B: Look inside the game
The puzzles are fun. The more interesting part is how
the program works. Open grid_world.py in Thonny and
read through it.
What’s in the file
Roughly four sections:
- Setup — imports, screen, grid size, colors.
- Puzzle definitions — a list of puzzles, each defining start position, goal, and walls.
- Movement API — the
move_right(),move_left(), etc. functions. They don’t move the character immediately — they add an instruction to amoveslist. - Game loop — handles input (1-5, SPACE, R), runs
solve()when SPACE is pressed, animates the character through the moves list one by one.
Why “build a list of moves”?
When you call move_right(), the character doesn’t
instantly jump to the next cell. Instead, the function
records “move one cell right” in a list. Then the
game loop processes the list one move per (say) 200ms,
animating each step.
This is a cool design pattern: separate the “what to do” from the “when and how to do it.”
What’s each function look like?
A peek:
char_x = 0
char_y = 0
moves = []
def move_right():
moves.append("right")
def move_left():
moves.append("left")
# ... etc.
def reset():
global char_x, char_y, moves
char_x, char_y = current_puzzle["start"]
moves = []
Each move function just appends to the moves list. The game loop reads from the front of the list and animates each step.
Walls
Walls are stored as a set of (x, y) cell positions.
When the game tries to move the character into a wall
cell, it skips the move (the character “bumps” but
doesn’t go through).
Why is this useful to understand?
Two reasons:
- You’ll extend it next week. Adding a new puzzle, a new movement function, or a new feature requires knowing the structure.
- It’s a real example of separating concerns. The
student-facing API (
move_right()) is dead simple. The implementation (animate the list, check walls, etc.) is hidden. Real software design.
Stretch — write your own puzzle solution efficiently
Some puzzles can be solved more elegantly. Try:
- Puzzle 5 with a single
forloop and a sequence of moves inside. - Solving with functions you write yourself. For example:
def go_right_then_down(n):
for _ in range(n):
move_right()
for _ in range(n):
move_down()
def solve():
go_right_then_down(3)
go_right_then_down(2)
You’re using functions to build higher-level movement primitives. This is real programming.
Stretch — make a deliberately silly solution
Try solving a puzzle by:
- Going way past the goal and looping back.
- Going back and forth before reaching it.
- Using
move_left()aftermove_right()(cancel out).
Watch the animation. The character does exactly what you say. Even when it’s silly.
Wrap-up
Before we leave, share with the room:
- For everyone — which puzzle was your favorite?
- For the kids who used loops — did the code shrink?
- For the kids who wrote their own helper functions — show one off.
- Did anyone make a deliberately silly solution? Show the audience.
Today you played a code-to-control-a-character game — like the ones online learn-to-code sites use, but you typed real Python (no blocks). And you peeked at the code that makes it work.
You also saw a real software pattern: a small,
clean API (the move_X() functions) hiding a more
complex implementation (the animation loop, wall
checking, multiple puzzles). That’s how good libraries
are designed.
Next week you’ll extend the grid-world. Add new puzzles. Add new movement abilities. Make it yours.
If you missed this session
You’ll need the grid_world.py file. Get it from a
classmate or from the class shared folder.
-
Open
grid_world.pyin Thonny. Run it. -
Try each puzzle. Press 1-5 to switch.
-
Edit
solve()to write code that gets the character to the goal. Press SPACE to run. -
Read through the code. Try to figure out where the movement API is implemented.
About 45-60 minutes. By the end you should have solved all five puzzles and understand the structure of the program.
Stretch and extension ideas
- Solve every puzzle in fewer lines. Use loops and helper functions. Code golf with intent.
- Solve every puzzle without using
move_left()ormove_up(). (Backtracking only when forced.) - Write a single
solve()that handles multiple puzzles. (Hard — will need conditionals based on current puzzle.) - Read every line of
grid_world.py. Make sure you could explain each section. - Add comments where the code is unclear. Good practice — and helpful for next week’s work.
What’s next
Next week: extend the grid-world. You’ll add at least one puzzle of your own design. The kids who go deeper will add new movement abilities (turn left, collect a gem, jump over a tile). The grid-world stops being Mr. Eric’s and starts being yours.
Session 11: The grid-world (extending)
Session 11: The grid-world (extending)
Phase 6 — Pygame · Session 11 of 14
What we’re learning today
Last week you played the grid-world. This week you make it yours. You’ll add at least one new puzzle of your own design. Going further: add a new movement ability, a new game element (a gem to collect, a tile that does something), or change how a puzzle works. By the end you’ll have a grid-world you designed — the kind of educational toy you used to be a user of.
This is the creator session.
You’ll need to remember from last time
- Grid-world basics — Session 10.
- The structure of
grid_world.py— puzzles, movement API, game loop. - Sprite classes (Session 9) — for new game elements.
- Lists, dictionaries, conditionals, functions — Phase 3.
Part A: Add a puzzle
The simplest extension. Each puzzle is just data — start position, goal position, and a list of walls. Adding one is a few lines.
Find the puzzles list
Open grid_world.py. Find where the puzzles are defined
— probably near the top, looking something like:
puzzles = [
{
"name": "Straight line",
"start": (0, 0),
"goal": (3, 0),
"walls": []
},
{
"name": "Turn",
"start": (0, 0),
"goal": (4, 4),
"walls": []
},
# ... etc
]
(Yours might look slightly different. The point is: each puzzle is a dictionary with a few key bits of data.)
Add your own
Add a new dictionary to the list. Pick a start, a goal, and any walls you want:
{
"name": "My puzzle",
"start": (0, 7),
"goal": (7, 0),
"walls": [(2, 5), (3, 5), (4, 5), (4, 4), (4, 3)]
},
The walls are (x, y) cells. Sketch on paper first if
it helps.
Save. Run grid_world.py. Your puzzle should appear as
the next number (if you had 5 puzzles, your new one is
puzzle 6 — press 6 to load it).
Solve your own puzzle. Make sure it’s actually solvable.
Test it on a friend
Have your buddy try to solve your puzzle without you telling them how. Notice:
- Was it too easy? Too hard?
- Did your buddy try a path you didn’t expect?
- Were the walls placed cleanly?
Iterate. Adjust walls or goal position. You’re now designing levels. This is real game design work.
Stretch — a series of puzzles with a theme
Add 3-5 puzzles that get progressively harder. A “level pack” you designed.
Checkpoint: You’ve added at least one puzzle of your own design. This is the natural stop point if class is cut short.
Part B: Add a feature
The puzzles work. Now add a new capability to the game. Pick one or more.
Option 1 — collect a gem
Add a “gem” to puzzles. Walking onto its cell collects it. Goal: reach the goal with the gem.
Steps:
-
Add
gemsto puzzle dicts (a list of cells where gems are):{ "name": "Gem run", "start": (0, 0), "goal": (7, 7), "walls": [], "gems": [(3, 3), (5, 5)] }, -
Track collected gems in game state:
collected_gems = set() -
When the character moves to a gem cell, collect it:
# In the move-step code: if (char_x, char_y) in current_puzzle.get("gems", []): collected_gems.add((char_x, char_y)) -
Check at the end: only “solved” if all gems collected:
if (char_x, char_y) == current_puzzle["goal"]: if len(collected_gems) == len(current_puzzle.get("gems", [])): print("Solved!") else: print("You missed a gem.") -
Draw gems in the draw loop (a yellow dot in each gem cell). Don’t draw the ones already collected.
This adds real puzzle design — the player has to plan a path that includes detours.
Option 2 — turn-based movement
Instead of move_left() and move_right(), give the
character a facing direction. Add turn_left(),
turn_right(), and forward().
Steps:
-
Track facing direction in game state:
facing = "right" # or "left", "up", "down" -
Add the new functions:
def turn_left(): moves.append("turn_left") def turn_right(): moves.append("turn_right") def forward(): moves.append("forward") -
Handle the new move types in the playback:
if move == "turn_left": # rotate facing direction directions = ["up", "left", "down", "right"] facing = directions[(directions.index(facing) + 1) % 4] if move == "turn_right": directions = ["up", "right", "down", "left"] facing = directions[(directions.index(facing) + 1) % 4] if move == "forward": # move in the current facing direction if facing == "right": char_x += 1 elif facing == "left": char_x -= 1 # ... etc -
Draw the character with a small arrow showing facing.
Now puzzles can require spatial reasoning — “forward, turn left, forward” instead of memorizing cardinal directions.
Option 3 — ice tiles
Some cells are “ice.” Stepping on ice slides the character one extra cell in the same direction.
Steps:
-
Add
iceto puzzles (a list of icy cells). -
After each move, check if the character is on ice:
if (char_x, char_y) in current_puzzle.get("ice", []): # slide one more cell in the same direction if last_direction == "right" and not blocked: char_x += 1 # ... etc -
Draw ice tiles with a different color (light cyan).
Ice changes the game from “execute moves” to “predict slides.” Real puzzle-game mechanic.
Option 4 — multi-character
Add a second character. Both have to reach goals.
Steps:
-
Add second character position and goal to puzzles.
-
Two sets of movement functions:
def move_right(): # moves character 1 moves.append(("char1", "right")) def move_right_2(): # moves character 2 moves.append(("char2", "right")) -
Playback handles both characters separately.
Extension — visual polish
Make the grid-world look nicer. Some ideas:
- Better character sprite (your own art).
- Animated background.
- Sound effects on each move (Session 8).
- Particle effect when goal is reached.
- A “moves remaining” counter (limit moves per puzzle).
- A “best solution” tracker (fewest moves wins).
Wrap-up
Before we leave, share with the room:
- For everyone — show your new puzzle. Have a buddy try it.
- For the kids who added a feature — demo it.
- What’s the most creative puzzle in the room?
- For kids who added gems / ice / facing direction — did your design make new kinds of puzzles possible?
Today you went from using Mr. Eric’s grid-world to building on it. You added puzzles. Maybe new abilities. Maybe new game elements.
This is the loop of real software work:
- Use a thing.
- Read its code.
- Modify it.
- Add to it.
- Now it’s yours.
Every program you’ll ever work on professionally started out as someone else’s. The skill is being able to step into and extend that work. You just did that.
Three sessions left in Phase 6:
- Session 12: Game state — title screens, game over, multiple “scenes.”
- Sessions 13-14: Your milestone game.
You have everything you need now. The milestone is about putting it all together.
If you missed this session
You’ll need grid_world.py (your modified version from
last week, or a fresh copy from the class folder).
-
Read through the file. Find the puzzles list.
-
Add at least one new puzzle. Solve it. Have a friend try.
-
(Stretch) Pick one feature from Part B (gems, facing, ice, multi-character) and implement it.
About 60-90 minutes. By the end you should have a grid-world that’s noticeably yours.
Stretch and extension ideas
- All four feature options above combined.
- A puzzle editor — UI for designing puzzles visually instead of editing code. Click cells to place walls. Click to place start/goal/gems. Save to a file.
- Save and load puzzles from JSON — extend the puzzle data to a file that can be edited outside the code.
- A “share your puzzle” mode — print the puzzle data to console; classmate pastes it into their grid-world.
- Animation polish — character bobs while moving, glows when reaching goal.
- A scoring system — fewer moves = higher score.
- Push your modified grid-world to GitHub. It’s a real project now. (Session 7 callback.)
What’s next
Next week: game state — title screens, gameplay, game over screens. The structural pattern that turns your games from “single screen” into “real games with multiple scenes.”
Session 12: Game state — title screens and game over
Session 12: Game state — title screens and game over
Phase 6 — Pygame · Session 12 of 14
What we’re learning today
Real games aren’t just one screen. They have a title screen when you launch them, gameplay when you press start, and a game over screen when you lose. Maybe a pause screen, a settings menu, a level-select. Today you learn the pattern that makes this work — a state variable that controls which screen the game is showing — and add proper title and game-over screens to one of your games.
This is the difference between “a single-screen toy” and “a real game.”
You’ll need to remember from last time
- Sprite classes and groups — Session 9.
- One of your previous games — Pong, fruit catcher, collector. Bring whichever you want to upgrade.
- The frame loop — input, update, draw.
- Conditionals —
if,elif,else.
Part A: A state variable
What “state” means here
A state is “what mode is the game in right now?” Title? Playing? Paused? Game over? Each state has different things drawn and different rules for input.
The simplest implementation: a string variable that says which state we’re in. The main loop checks the variable and dispatches.
A tiny example — title and play
Open Thonny. Save a new file as state_demo.py. Type:
import pygame
pygame.init()
screen = pygame.display.set_mode((600, 400))
pygame.display.set_caption("State demo")
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 36)
big_font = pygame.font.SysFont("Arial", 72)
state = "title"
x = 300
y = 200
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and state == "title":
state = "playing"
if event.key == pygame.K_ESCAPE and state == "playing":
state = "title"
# Update and draw based on state
screen.fill((30, 30, 50))
if state == "title":
title = big_font.render("MY GAME", True, (255, 255, 255))
sub = font.render("Press Enter to play", True, (180, 180, 180))
screen.blit(title, title.get_rect(center=(300, 150)))
screen.blit(sub, sub.get_rect(center=(300, 250)))
elif state == "playing":
# Update — move the box
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]: x -= 5
if keys[pygame.K_RIGHT]: x += 5
if keys[pygame.K_UP]: y -= 5
if keys[pygame.K_DOWN]: y += 5
# Draw — show the box
pygame.draw.rect(screen, (255, 100, 50), (x, y, 50, 50))
hint = font.render("Esc = back to title", True, (180, 180, 180))
screen.blit(hint, (10, 10))
pygame.display.flip()
clock.tick(60)
pygame.quit()
Save. Run. You see a title screen with “MY GAME” and “Press Enter to play.”
Press Enter. State flips to "playing". The orange
box appears. Use arrow keys to move it.
Press Esc. State flips back to "title". The title
screen returns.
That’s it. That’s the whole pattern.
What’s happening
state = "title"— a variable that holds the current state name (a string).if state == "title": ...— when in title state, do title things (draw the title, check for Enter).elif state == "playing": ...— when in playing state, do game things (move the box, draw the box).- State transitions happen in the input handling — Enter sends you from title to playing; Esc sends you back.
It’s that simple. A string. An if/elif. Transitions
on input.
Add a third state — game over
# At the top, add:
score = 0
# Inside event handling, add:
if event.key == pygame.K_RETURN and state == "game_over":
# restart
state = "title"
score = 0
# Inside the playing state's update, add a way to lose:
if x < 0 or x > 550 or y < 0 or y > 350:
state = "game_over"
# Add the game_over branch to your dispatcher:
elif state == "game_over":
over = big_font.render("GAME OVER", True, (255, 50, 50))
sub = font.render("Press Enter to restart", True, (180, 180, 180))
screen.blit(over, over.get_rect(center=(300, 150)))
screen.blit(sub, sub.get_rect(center=(300, 250)))
Save. Run. Move the box off-screen → game over screen. Press Enter → back to title. Replay.
You now have a complete game flow:
title → playing → game_over → title → ...
Three states. Three transitions. Looks like a real game.
Checkpoint: You have a program with three states and you can transition between them all. This is the natural stop point if class is cut short.
Part B: Add states to a real game
Pick one of your games — Pong, fruit catcher, the collector — and add a title screen and game-over screen. Open the file. Make a copy first if you want.
Adding a title to Pong
Above the loop:
state = "title"
font = pygame.font.SysFont("Arial", 36)
big_font = pygame.font.SysFont("Arial", 80)
Convert your existing main loop logic into an
elif state == "playing": block. Add a title state in
front and (likely) a game-over state at the end.
Sketch:
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_RETURN and state == "title":
state = "playing"
# reset whatever needs resetting (scores, ball position)
if event.key == pygame.K_RETURN and state == "game_over":
state = "title"
screen.fill(BLACK)
if state == "title":
title = big_font.render("PONG", True, WHITE)
sub = font.render("Press Enter to play", True, WHITE)
screen.blit(title, title.get_rect(center=(WIDTH//2, HEIGHT//2 - 40)))
screen.blit(sub, sub.get_rect(center=(WIDTH//2, HEIGHT//2 + 40)))
elif state == "playing":
# ALL your existing Pong code goes here:
# paddle movement, ball update, collision, score draw, ...
# Check for game over
if left_score >= 10:
state = "game_over"
winner = "Left player"
elif right_score >= 10:
state = "game_over"
winner = "Right player"
elif state == "game_over":
text = big_font.render(f"{winner} wins!", True, WHITE)
sub = font.render("Press Enter to restart", True, WHITE)
screen.blit(text, text.get_rect(center=(WIDTH//2, HEIGHT//2 - 40)))
screen.blit(sub, sub.get_rect(center=(WIDTH//2, HEIGHT//2 + 40)))
pygame.display.flip()
clock.tick(60)
Save. Run. Title screen → press Enter → play → first to 10 → game over → press Enter → title.
Your Pong now feels like a real game.
Resetting state on transitions
A common bug: you transition to "playing" but the
score from the last game carries over. Or the ball is
still in the position from when you lost.
Solution: do the reset as part of the transition.
if event.key == pygame.K_RETURN and state == "title":
state = "playing"
left_score = 0
right_score = 0
ball_rect.center = (WIDTH//2, HEIGHT//2)
# ... reset whatever else
A cleaner approach: write a reset_game() function and
call it. When the game grows, you’ll thank yourself.
def reset_game():
global left_score, right_score
left_score = 0
right_score = 0
ball_rect.center = (WIDTH//2, HEIGHT//2)
# ... etc
Then reset_game() on transition.
Stretch — add a pause state
A fourth state: paused. Press P to pause; press P to resume.
# In event handling:
if event.key == pygame.K_p:
if state == "playing":
state = "paused"
elif state == "paused":
state = "playing"
# Add the dispatcher branch:
elif state == "paused":
# Draw the game frozen (snapshot from last frame stays on screen)
# Then draw "PAUSED" overlay
text = big_font.render("PAUSED", True, WHITE)
screen.blit(text, text.get_rect(center=(WIDTH//2, HEIGHT//2)))
# Notice: no game updates happen this frame
Pause = “skip the update; just keep drawing.”
Stretch — high score display on title
Save the high score to a file (Phase 5 callback). Show it on the title screen.
import os
def load_high_score():
if os.path.exists("highscore.txt"):
with open("highscore.txt") as f:
return int(f.read())
return 0
def save_high_score(score):
with open("highscore.txt", "w") as f:
f.write(str(score))
# At startup:
high_score = load_high_score()
# When game ends:
if score > high_score:
high_score = score
save_high_score(high_score)
# On title screen, display:
hs_text = font.render(f"High score: {high_score}", True, WHITE)
screen.blit(hs_text, hs_text.get_rect(center=(WIDTH//2, HEIGHT//2 + 100)))
Now your game persists between runs. The high score follows you.
Stretch — class-based scenes
For bigger games, the if/elif dispatcher gets unwieldy.
Real production games use scene classes — each scene
is a class with update() and draw() methods, and
the main loop calls the current scene’s methods.
class TitleScene:
def update(self, events):
for event in events:
if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
return PlayingScene()
return self
def draw(self, screen):
title = big_font.render("PONG", True, WHITE)
screen.blit(title, title.get_rect(center=(WIDTH//2, HEIGHT//2)))
class PlayingScene:
def __init__(self):
# set up sprites, scores, etc.
...
def update(self, events):
# game logic
if won:
return GameOverScene()
return self
def draw(self, screen):
# draw sprites
...
# In the main loop:
current_scene = TitleScene()
while running:
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
current_scene = current_scene.update(events)
screen.fill(BLACK)
current_scene.draw(screen)
pygame.display.flip()
clock.tick(60)
The trick: each scene’s update() returns either
self (stay in this scene) or a new scene (transition
to that one).
This pattern scales much better. For a 3-state game, the if/elif version is fine. For a game with title, level-select, playing, paused, game-over, victory, credits… use scene classes.
Wrap-up
Before we leave, share with the room:
- For everyone — show your game with a title screen. Did it feel different to launch?
- For the kids who added pause — pause feels weirdly satisfying, doesn’t it?
- For the kids who saved a high score — show closing the game and reopening to see the score persist.
- Anyone try the scene-class pattern?
Today you learned:
- A state variable controls which screen the game is showing.
if/elifdispatch on the state variable.- State transitions happen on input or game events (and may also reset state).
- Resetting state is part of the transition.
- (Stretch) Scene classes — scalable pattern for bigger games.
You also turned at least one previous game from “single screen toy” into “real game with proper screens.” That’s the polish that makes a game feel finished.
Next two weeks are your milestone game. You have every tool you need:
- Sprites, classes, groups (Session 9)
- Input + collision (Sessions 4-5)
- Sound (Session 8)
- Game state (today)
- Git + GitHub (Session 7)
Bring an idea. Or come without one — we’ll have a seed list ready.
If you missed this session
Open Thonny.
-
Build the basic
state_demo.pyfrom Part A. Get the title → playing → game_over flow working. -
Pick one of your games and add a title screen and game-over screen.
-
(Stretch) Add a pause state.
-
(Stretch) Save a high score to a file.
About 60-90 minutes — this is meaty.
Stretch and extension ideas
- Pause state with a translucent overlay over the paused gameplay.
- High score persistence to a file.
- Settings screen with options (volume, color scheme, difficulty).
- Level select screen between title and playing.
- Credits screen accessible from the title.
- Animated transitions between scenes — fade out the old, fade in the new.
- Scene classes for any game with 4+ states.
- Push your improved game to GitHub. A second commit on the same repo. Real iteration history.
What’s next
Next week is milestone planning + work day 1. You plan and start your Pygame game — your design, your art, your gameplay. Week after, you finish, polish, and demo to the class.
Bring an idea or two. Or come empty-handed — we have a seed list ready.
Session 13: Milestone project work day 1
Session 13: Milestone project work day 1
Phase 6 — Pygame · Session 13 of 14
What we’re learning today
Today is your day. You’ll plan a Pygame game — your design, your art, your gameplay — and start building it. Next week you’ll finish it and demo it to the class.
This is your sixth milestone project. You have every tool you need.
You’ll need to remember from last time
- The frame loop (Session 1) — input, update, draw.
- Drawing and sprites (Sessions 2-3).
- Input (Session 4) — events vs polling.
- Collision (Session 5).
- Sound (Session 8) —
mixer.Sound,mixer.music. - Sprite classes and groups (Session 9).
- Game state (Session 12) — title, playing, game-over.
- Git + GitHub — commit as you go; push to a repo by the end.
Part A: Plan your game
Same shape as previous milestones, with new requirements specific to Phase 6.
The plan
Take a piece of paper or open a blank text file. Answer these seven questions:
-
What’s the game? (One sentence. “Snake but with power-ups.” “A two-player tank battle.” “A puzzle where you arrange tiles.”)
-
What does the player do? (Move with arrows? Click to shoot? Type a word?)
-
What’s the goal? What’s the lose condition? (Reach a score? Survive 60 seconds? Clear all levels?)
-
What sprites are in it? (Player? Enemies? Items? Backgrounds? Sketch the screen.)
-
What states does it have? (Title, playing, game-over at minimum. Maybe more.)
-
What’s the simplest version? (Build this FIRST. No art polish, no sound, no fancy features. Just core gameplay.)
-
What’s one stretch feature? (After core works: sound? high score? AI opponent? more levels?)
Phase 6 requirements
Your milestone must have:
- At least one sprite class (Session 9).
- At least three game states — title, playing, game-over (Session 12).
- Pushed to GitHub by the demo (Session 7).
- Run without crashing under normal use.
- Be fun for at least 30 seconds. Subjective but real. If your buddy can’t enjoy it for 30 seconds, iterate.
If you don’t have an idea
Pick one and modify:
- Snake — classic. Player snake grows when eating apples; lose if you hit yourself or the wall.
- Brick Breaker — Pong with a wall of bricks at the top. Hit the ball to break bricks. Win when all bricks are broken.
- Space Invaders simple — a ship at the bottom, enemies advancing from the top. Shoot bullets up; enemies shoot down.
- Frogger — guide a frog across a road / river of moving obstacles. Don’t get hit.
- A puzzle game — Sokoban-style boxes-and-floors, match-3, sliding tiles. (The grid-world from Sessions 10-11 was this kind!)
- A two-player game — tank battle, sumo (push each other off), maze-chase, anything that needs two controllers (WASD vs arrows).
- A reaction game — circles appear and disappear; click them before they vanish. Faster as you go.
- A flappy-bird-style game — gravity pulls a bird down; tap to flap; avoid pipes.
- A rhythm game — notes scroll; press the right key at the right time.
- A platformer (small) — single screen, jump on platforms, reach the goal. Don’t fall.
- Asteroids variant — ship in the middle, asteroids fly around. Shoot them. Don’t get hit.
- A shoot-em-up bullet hell — enemies come from above; you dodge bullets, return fire.
- A grid-world variant — start with
grid_world.py, but make it about your theme (maze for a wizard, delivery routes for a robot, etc.).
Pick one. Spend two minutes. Don’t overthink.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question.
Set up the project
Create a folder for your project:
$ cd ~ # or wherever you keep projects
$ mkdir my_game
$ cd my_game
$ git init
$ touch main.py # create empty file
$ git add main.py
$ git commit -m "Initial project setup"
Build the simplest version first
Look at your answer to question 6. Build that first. Get a window. Get a player on screen. Get one core mechanic working. Iterate from there.
For each significant change:
- Make the change.
- Run it. Make sure it works.
- Commit.
- Move to the next thing.
If something feels risky, make a branch:
$ git checkout -b try-new-thing
# experiment
$ git checkout main
$ git merge try-new-thing # if it worked
# OR
$ git branch -D try-new-thing # if not
Use what you’ve learned
- Sprite classes for any game object that has position + behavior (player, enemies, items, bullets).
- Groups for collections (
enemies = pygame.sprite.Group()). spritecollidefor collisions.- State variable for screens.
- Sound for at least one event (Session 8) — even one sound makes the game feel alive.
The complete list of patterns is in Sessions 1-12. Skim them when stuck.
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today.
Bring your project (the folder — Git carries the history) next week. We’ll finish, then demo.
If you got far, push to GitHub before next week. You can also keep iterating at home if you have Pygame installed.
If you missed this session
Open Thonny and a terminal.
-
Spend 10-15 minutes answering the seven planning questions.
-
Create a folder for your project.
git initin it. Createmain.py. Commit. -
Build the simplest version (question 6). Commit as you go.
About 60-90 minutes total. By next week you should have:
- A window with at least the player on it.
- One core mechanic working (player can move, or one collision happens, or one rule is enforced).
- Several Git commits.
If you don’t have an idea, pick from the seed list and modify.
Stretch and extension ideas
If your base game is working and you want to add more:
- Sprite classes for everything — player, enemies, items.
- Sound effects for at least three events.
- Background music that loops.
- All three states — title, playing, game over.
- High score persistence to a file.
- Push to GitHub — your game becomes a public repo.
- README.md with controls and how to run.
- Polish — better art, smoother animations, juicier feedback.
- Pause state.
- Difficulty progression — gets harder over time.
What’s next
Next week you’ll have time to finish, polish, and demo your game to the class. Each person gets 3-5 minutes. Bring a working game, your enthusiasm, and your GitHub URL — part of the demo will be showing the journey of how your game came together.
Session 14: Milestone project work day 2 + demo day
Session 14: Milestone project work day 2 + demo day
Phase 6 — Pygame · Session 14 of 14
What we’re learning today
Today is the last session of Phase 6 — and the midpoint of the entire curriculum. First half: finish your game. Second half: demo to the class. By the end, you’ll have shipped your sixth milestone project, pushed it to GitHub, and you’re halfway through the journey from “no programming experience” to “shipping real software.”
You’ll need to remember from last time
- Your project plan from last week.
- Whatever you got working last week.
- Your buddy for testing.
- Git — commit your final changes today.
- GitHub — push the final version.
- Everything from Sessions 1-12 — you have all the tools.
Part A: Final polish
You have about 35 minutes to finish.
What “finished” means
Same three rules as always:
- It runs without crashing under normal use.
- It does what your plan said it would do — at least the simplest version.
- You can explain how it works.
If your game doesn’t meet all three, focus on getting there before adding anything new.
Polish ideas (Phase 6 specific)
- Add a title screen if you don’t have one. (Session 12 pattern.)
- Add a game-over screen with restart instructions.
- Add at least one sound — bounce, hit, score, death. (Session 8.)
- Background music that loops.
- Use sprite classes for your game objects (Session 9). If you started with raw rect lists, refactor.
- Reset on restart — make sure score, lives, positions reset properly between rounds.
- Window title and size.
pygame.display.set_caption( "...")andset_mode((W, H)). - Add a README.md with controls and how to run. Push it to your repo.
- Take a screenshot for the README.
- All commits clean.
git statusshould show “working tree clean” before the demo.
Buddy test
About 15 min in, swap with your buddy. They play your game; you play theirs. Notice things they get wrong (control confusion, unclear win condition). Those are real UX bugs.
For Pygame games especially: hand the keyboard to your buddy with no instructions. Watch what they try. Their first instinct = your game’s controls need to match.
Push to GitHub
If you haven’t already: push. Today is your demo day, and your demo includes showing your repo URL.
$ git status
$ git add .
$ git commit -m "Final polish before demo"
$ git push
If your repo doesn’t have a README, add one now:
# My Game
A Pygame game built in From Scratch Programming Class,
Phase 6.
## How to play
- **Move:** WASD or arrows
- **Action:** Space
- **Quit:** Esc
## How to run
pip install pygame python main.py
Push it.
Checkpoint: Your game runs the basic version of
what you planned, your git log shows the journey, and
your repo is on GitHub. This is the natural stop point
if class is cut short — but today, demo time is next.
Part B: Demo day
Each person gets 3-5 minutes.
How a demo works
When it’s your turn:
- Show your title screen. That first frame is the impression. Walk us through the controls if needed.
- Play the game. Let us see it actually working.
- Hand the keyboard to a classmate if there’s time. Real users, real reactions.
- Show your GitHub repo. Open the URL in a
browser. Show your
git log --oneline. - Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected. Maybe one thing that surprised you.
- Take one question.
What we’re celebrating
This is Phase 6’s demo — and the midpoint of the entire curriculum. Halfway through. You’ve shipped:
- A Scratch project (Phase 1).
- A Python turtle scene (Phase 2).
- A Python program (Phase 3).
- A multi-file CLI tool (Phase 4).
- A desktop app (Phase 5).
- A game (Phase 6, today).
Six. Most adults have shipped zero. You’re at six.
After everyone demos
Mr. Eric will say a few specific things about each project. Then we celebrate Phase 6 completion. Then we frame Phase 7.
What you accomplished
- Sixth milestone project. Shipped.
- Sprite classes — you wrote production-style Pygame code.
- Game state machine — your game has real screens.
- Public GitHub repo with a real README and (likely) a screenshot.
- Animation, input, collision, sound — all of it working together.
- You finished Phase 6. Half the curriculum is yours.
What’s next: Phase 7 — HTML, CSS, JavaScript
Phase 7 moves to the web. You learn to build things that run in a browser — anyone with internet can visit and use, no install required.
What’s different:
- HTML for structure, CSS for style, JavaScript for behavior. Three new languages (well, technically — they’re all friendly).
- The browser is your runtime. No
pip install, no Python. Just a.htmlfile and a browser. - Designing for anyone. Your work is shareable as a URL. Friends, family, anyone in the world can use it from their phone or laptop.
- Visual design. CSS lets you make things look the way you want — colors, fonts, layouts, animations.
- JavaScript ≠ Python. Different syntax. Same ideas (variables, functions, loops, classes). The mental model from Phase 3 still applies.
You’ll build:
- Static pages.
- Interactive single-page apps.
- A canvas-based mini-game (yes, browser games — the Pygame patterns transfer).
- A milestone web project of your design, hosted on GitHub Pages so anyone can use it.
Phase 7 is longer than Phase 6 (about 17 sessions). It introduces the most languages of any phase. But you’ve got a strong foundation — every concept (loops, functions, classes, state machines) you’ve already met.
Bring your machine and your enthusiasm. See you in Phase 7.
If you missed this session
Two cases:
Missed only the demo half: Show your game to Mr. Eric at the start of next week’s class. Same kind of feedback you’d have gotten in the demo.
Missed the whole session: No big deal. Finish your game at home using your Session 13 plan, and bring it next week. Or just join us in Phase 7 — you’ve already shipped enough this phase to be proud.
Stretch and extension ideas
If you have time after demos, or want to keep building at home:
- Polish more. Add features, more sound, better art, smoother animation.
- Share your game. Send the GitHub URL to a friend or family member. Have them play.
- Build a release. Tools like PyInstaller can turn
your game into a standalone
.exeor.app. Advanced — ask Mr. Eric. - Add controller support. Pygame supports gamepads
via
pygame.joystick. - Open source someone else’s Pygame project. Find one on GitHub, clone it, run it, modify it.
- Save a copy of every milestone project. They’re yours. Six so far.
- Read a Pygame tutorial outside our curriculum. Real Python, free YouTube series, official Pygame docs. Different teachers explain differently.
What’s next
Phase 7 — the web. See you there.
Phase 7 — HTML, CSS, JavaScript
You can build games. You can write desktop apps. Now we make things for the browser — websites and web apps that anyone with internet can visit. No installs, no “first set up Python.” Just a URL.
This is the longest phase yet because the web is three languages and a lot of new vocabulary. But you have a strong foundation: every concept (variables, loops, functions, classes, state machines) shows up here, just in new syntax.
What this phase is
About seventeen sessions across three movements:
- HTML + CSS (Sessions 1-7) — structure and style. Building things you can see in a browser.
- JavaScript (Sessions 8-11) — adding behavior to the page. Buttons that do things. Forms that respond. Pages that update without reloading.
- Real apps (Sessions 12-15) — Canvas mini-games, hitting an API, deploying to the world.
Then your milestone — your own web app or game, hosted on a public URL anyone can visit.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to the web — first HTML page | HTML basics, the browser as runtime |
| 2 | HTML tags and structure | Semantic tags, nesting, attributes |
| 3 | CSS basics — selectors, colors, fonts | The cascade, selectors, declarations |
| 4 | The box model | padding, margin, border |
| 5 | Flexbox layout | Modern layout |
| 6 | Build a personal homepage | HTML + CSS integration project |
| 7 | Forms — inputs, labels, buttons | Forms that look right |
| 8 | JavaScript — syntax (compared to Python) | let/const, semicolons, braces |
| 9 | The DOM — querySelector and events | Reach into the page from JS |
| 10 | Build an interactive todo list | JS integration project |
| 11 | localStorage — persistence | State that survives reloads |
| 12 | Canvas — drawing in JS | Like Pygame’s draw, in the browser |
| 13 | Canvas mini-game | Frame loop, sprites, collision — in the browser |
| 14 | Fetch + JSON — hitting an API | Talk to the world |
| 15 | GitHub Pages — host your work | Your URL on the internet |
| 16 | Milestone day 1 | Plan + build |
| 17 | Milestone day 2 + demo | Finish + showcase |
What you’ll build
- Sessions 1-7: small experiments — text pages, styled cards, layouts, forms.
- Session 6: a personal homepage — your first real artifact, pure HTML+CSS.
- Sessions 8-11: small interactive things — counters, calculators, todo lists.
- Session 13: a Canvas mini-game — Pong or similar, but in the browser this time.
- Sessions 16-17: your milestone web project — your design, hosted on a public URL.
What you’ll need
- Same machine as before. No new installs needed. Web stuff runs in the browser — Firefox or Chrome both work.
- Thonny still works as your editor (any text editor does, but Thonny is familiar).
- Your GitHub account from Phase 6 — we’ll use GitHub Pages for hosting.
- Your favorite browser open and ready.
How sessions work
Same shape as before:
- Part A introduces a concept with a guided exercise.
- Part B is open practice or a project.
- Wrap-up to share what you did.
How the web is different
A few notable shifts:
Three languages, not one
- HTML — the structure (what’s on the page).
- CSS — the style (how it looks).
- JavaScript — the behavior (what it does when you click).
These three work together. A web page is all three.
The browser is your runtime
In Phase 6, you ran python game.py. In Phase 7, you
double-click an HTML file (or open it in a browser
via file:///path/to/index.html). The browser reads
the HTML, applies the CSS, runs the JavaScript.
No python command. No Pygame install. Just a file +
a browser.
Your work is shareable as a URL
Once you push to GitHub Pages (Session 15), your work
has a real public URL like
https://YOUR-USERNAME.github.io/my-site. Send that
link to anyone — they can use your site, no install
needed.
This is a huge power. Phase 6 games run on your machine; Phase 7 sites run anywhere.
JavaScript is not Python
Same ideas, different syntax:
# Python
for i in range(10):
print(i)
// JavaScript
for (let i = 0; i < 10; i++) {
console.log(i);
}
You’ll spend Session 8 on the syntax differences. After that the thinking is the same.
CSS is its own language
CSS doesn’t really look like a programming language — no loops, no conditions (well, a few — but rare). It’s more like a list of rules: “this element looks this way.” You’ll get used to it fast.
What we’re skipping
To keep things hardware-friendly and approachable, we skip a lot of modern web tooling:
- No Webpack, Vite, Parcel, or any build tool.
- No npm, no
package.json, nonode_modules. - No React, Vue, Angular, or Svelte.
- No TypeScript.
- No CSS frameworks (Bootstrap, Tailwind).
- No CSS Grid (Flexbox covers what we need).
- No CSS preprocessors (Sass, Less).
This is the original web — HTML, CSS, JavaScript, loaded directly from files. It’s what every website compiles down to eventually. Frameworks are useful once you’re scaling to large teams or apps, but they hide what’s really happening. You’ll learn the underlying machinery first.
If you want to learn React or another framework after the curriculum, the foundation here makes that much easier.
A note about getting frustrated
The web has way more vocabulary than Python or Pygame. HTML elements, attributes, CSS properties, JS methods — hundreds of names. Don’t try to memorize them all.
Real web developers look things up constantly. The sites everyone uses:
- MDN Web Docs — the reference. Look up any HTML/CSS/JS thing.
- caniuse.com — does this work in browsers?
- css-tricks.com — guides and tutorials.
You’ll bookmark these.
Where to start
Session 1: Welcome to the web opens your first HTML page in a browser.
When you’re stuck, the Getting unstuck appendix is the first place to go. The Glossary will grow as Phase 7 introduces web-specific terms.
Welcome to the web. Let’s go.
Session 1: Welcome to the web — your first HTML page
Session 1: Welcome to the web — your first HTML page
Phase 7 — Web · Session 1 of 17
What we’re learning today
You’ve been writing programs — Python that you run
with python game.py. The web works differently.
Today you’ll write your first HTML file, open it in
a browser, and watch the browser turn your text into
a real page. By the end of class, you’ll understand
what HTML is, what CSS is, what JavaScript is, and
how all three work together.
You’ll also build a (very small) personal page about yourself.
You’ll need to remember from last time
- The terminal (Phase 4 Session 1).
- A text editor — Thonny works fine.
- Your favorite browser.
- The general idea of files and folders.
What you’ll need today
- A web browser (Firefox, Chrome, Edge — all work).
- Thonny (or any text editor).
Part A: Your first HTML page
What’s HTML?
HTML stands for HyperText Markup Language. It’s the structure of every web page in existence — what’s on the page, organized into headings, paragraphs, lists, links, images, and so on.
When you visit a website, the first thing the browser downloads is HTML. The browser reads the HTML and shows you the page.
HTML uses tags to mark up content. A tag looks like
<tagname>...</tagname>. The opening tag, the
content, the closing tag. Examples:
<h1>Hello!</h1>— a level-1 heading.<p>Some text.</p>— a paragraph.<a href="...">click me</a>— a link.
Make your first page
Open Thonny. Create a new file. Type this:
<!DOCTYPE html>
<html>
<head>
<title>My first page</title>
</head>
<body>
<h1>Hello, web!</h1>
<p>This is my first HTML page.</p>
</body>
</html>
Save it as index.html in a new folder somewhere
you can find — maybe ~/web/first-page/index.html.
Now open the file in your browser. Three ways:
- In your file manager, double-click
index.html. - In your browser, File → Open File → pick the
index.html. - Drag the file from the file manager into the browser window.
You should see:
Hello, web!
This is my first HTML page.
The text “Hello, web!” appears as a big heading. The text “This is my first HTML page.” appears as a normal paragraph.
Congratulations. You wrote a web page.
What every line does
<!DOCTYPE html>
Tells the browser: “this is HTML5, the modern version.” Always include this at the very top.
<html>
...
</html>
The root tag — everything wraps inside this. Like the outer envelope of the page.
<head>
<title>My first page</title>
</head>
The <head> is invisible stuff about the page —
metadata, the title (shows in the browser tab), links
to CSS, scripts, etc. Anything inside <head> does
not show up on the page itself.
<body>
<h1>Hello, web!</h1>
<p>This is my first HTML page.</p>
</body>
The <body> is the visible stuff. Headings,
paragraphs, images, buttons — everything the user sees.
This shape — <html> containing <head> and
<body> — is the standard skeleton of every HTML
page in the world. You’ll write it constantly. Memorize
it.
Edit and reload
Change the text. Add another paragraph:
<body>
<h1>Hello, web!</h1>
<p>This is my first HTML page.</p>
<p>I am learning HTML in class.</p>
</body>
Save. Reload the browser (F5 or Ctrl+R / Cmd+R). The new paragraph appears.
This is the core loop of web development:
- Edit the file.
- Save.
- Reload the browser.
- See the change.
Repeat forever.
Add some structure
Try more tags:
<body>
<h1>About me</h1>
<p>My name is Alex. Today I am learning HTML.</p>
<h2>Things I like</h2>
<ul>
<li>Pizza</li>
<li>Soccer</li>
<li>Reading</li>
</ul>
<h2>Things I don't like</h2>
<ul>
<li>Brussels sprouts</li>
<li>Loud noises</li>
</ul>
</body>
Save. Reload. You see two sections, each with a
heading (<h2>) and a bulleted list (<ul> containing
<li> items).
What’s new:
<h2>— a smaller heading. There’s<h1>through<h6>. Pick the right level for the importance.<ul>— an unordered list (bullet points).<li>— a list item. Goes inside<ul>(or<ol>for numbered lists).
A picture
Add an image. First, find or save an image file in your
folder (any PNG or JPG). Let’s say it’s named
me.png.
<img src="me.png" alt="A picture of me">
Save. Reload. The image appears.
Two new things:
<img>— a self-closing tag (no</img>). It’s empty — has no content between open/close tags.src="..."— an attribute. Says source — where to find the image file.alt="..."— alt text. What screen readers say when an image can’t be displayed. Always include it (real-world accessibility — and required for professional sites).
A link
<a href="https://en.wikipedia.org/wiki/Web_browser">What's a browser?</a>
Save. Reload. The text “What’s a browser?” appears as a link (usually blue and underlined). Click — your browser navigates to that page.
What’s new:
<a>— anchor (the original name for “link” before “link” became common). Wraps the clickable text.href="..."— hypertext reference. Where the link goes.
Checkpoint: You have an HTML page with a heading, paragraphs, a list, an image, and a link. This is the natural stop point if class is cut short.
Part B: A page about you
Time to make it yours. Build a personal page about yourself.
Required pieces
- One
<h1>with your name (or a nickname — your choice). - At least two
<h2>section headings. - At least 3 paragraphs total.
- One
<ul>list with at least 3 items. - One image (find one online, save to your folder, or use one already there).
- One link (to anything you find interesting).
Encourage personality
Tell us things. Hobbies, favorite games, books, weird opinions. Make it yours — not a template.
Examples of sections:
- “About me”
- “Games I play”
- “Books I’m reading”
- “Music I like”
- “What I’m building right now”
- “Cool stuff online”
Save it
Name the file index.html. We’ll come back to this
page over the next few sessions and style it (Session
3 onward) and add interactivity (Sessions 8 onward).
By Session 6 you’ll have your real personal homepage.
Stretch — more tags
Try these:
<strong>important text</strong>— bold, emphasized.<em>italics</em>— italic, lightly emphasized.<br>— a line break (no closing tag).<hr>— a horizontal rule (a divider line).<ol>— ordered (numbered) list. Same<li>s inside.<blockquote>— for quotes.
Look up any of them on MDN.
Stretch — view source on a real site
Open any website you like in a browser. Right-click → View Page Source (or press Ctrl+U / Cmd+U). You see the HTML the website is made of.
Real websites are huge HTML documents — sometimes
thousands of lines. But the basic shape is the same:
<html> containing <head> and <body>, with tags
inside. You can read any of it.
This is one of the great things about the web — every website’s source is visible. You can learn from any of them.
Extension — add comments
HTML supports comments — text the browser ignores. Useful for notes-to-self:
<!-- This is a comment. The browser doesn't show it. -->
<h2>Things I like</h2>
<!-- TODO: add more sections later -->
Comments use <!-- ... -->. Useful for explaining
tricky markup or marking sections.
Wrap-up
Before we leave, share with the room:
- For everyone — show your page. What did you put on it?
- Did you find the file → save → reload loop natural?
- Anyone view-source on a real website? See anything surprising?
Today you learned:
- HTML is the structure of every web page.
- Tags look like
<tagname>...</tagname>. - The skeleton —
<!DOCTYPE html>,<html>,<head>,<body>. - Common tags —
<h1>-<h6>,<p>,<ul>/<li>,<img>,<a>. - Attributes —
src=,href=,alt=. - The web dev loop — edit, save, reload.
You wrote a page that runs in any browser on any machine in the world. That’s the magic of the web — universal, no install, just text the browser interprets.
Next week: more tags, semantic HTML, and structuring a real page.
If you missed this session
Open Thonny.
-
Create a folder for web stuff. Inside it, create a file named
index.html. -
Type the basic HTML skeleton (DOCTYPE, html, head, title, body).
-
Add an
<h1>and a few paragraphs in the body. -
Save. Open
index.htmlin a browser by double-clicking. -
Build a small page about yourself with the required pieces from Part B.
About 30-45 minutes. By the end you should have an
index.html you can open in a browser.
Stretch and extension ideas
- More tags —
<strong>,<em>,<br>,<hr>,<ol>,<blockquote>. - Multiple pages — make
about.htmland link to it fromindex.html. Now you have a tiny site. - View source on a real website. Look at how it’s structured.
- HTML reference — bookmark MDN HTML elements list.
- Try special characters —
©(©),♥(♥),&(&),<(<),>(>). These are HTML entities — escape sequences for characters that would conflict with tag syntax.
What’s next
Next week: HTML tags and structure — semantic HTML
(<header>, <nav>, <main>, <footer>), nesting,
and how to organize a real-feeling page.
Session 2: HTML tags and structure
Session 2: HTML tags and structure
Phase 7 — Web · Session 2 of 17
What we’re learning today
Last week you wrote your first HTML page. Today we go
deeper into how HTML organizes a real page — the
common tags you haven’t met yet (tables, forms,
divs/spans), the semantic tags (<header>,
<nav>, <main>, <footer>) that real sites use, and
the idea of nesting tags inside each other to
build structure. By the end you’ll have a properly
structured page that’s ready to be styled next session.
You’ll need to remember from last time
- The HTML skeleton —
<!DOCTYPE>,<html>,<head>,<body>. - Common tags —
<h1>-<h6>,<p>,<ul>/<li>,<a>,<img>. - Attributes —
src=,href=,alt=. - The edit → save → reload loop.
Part A: More tags and nesting
Tags inside tags
You’ve already done this — <ul> contains <li>s.
That’s nesting — a tag inside another tag.
Nesting is how HTML builds structure. A page is a tree — tags inside tags inside tags, all the way down.
<body>
<h1>My page</h1>
<p>Here's a paragraph with <strong>bold text</strong> in it.</p>
</body>
The <strong> is inside a <p>. The <p> is
inside <body>. Nest however deep you need.
Indentation helps you see the structure: indent nested tags by 4 spaces (or 2 — pick one and stick to it). The browser doesn’t care about whitespace, but you (and other humans) do.
Inline tags
Some tags wrap a small bit of text inside a paragraph, without breaking onto a new line. These are inline tags:
<strong>...</strong>— important / bold.<em>...</em>— emphasized / italic.<a>...</a>— link.<code>...</code>— inline code (monospace).<span>...</span>— generic inline (for styling).
Other tags create their own block on the page — block tags:
<h1>through<h6>— headings.<p>— paragraph.<ul>,<ol>,<li>— lists.<div>— generic block (for structure).
The mental model: block tags stack vertically; inline tags flow with the text.
<div> and <span> — the generic ones
<div> (block) and <span> (inline) don’t mean
anything — they’re empty containers you wrap around
content for styling or grouping.
<div class="card">
<h2>A card</h2>
<p>Some content.</p>
</div>
We’ll use class="..." heavily next session for CSS.
For now, know that <div> is a “this stuff goes
together” wrapper.
Tables
For real tabular data — like a schedule or a comparison
chart — use <table>:
<table>
<tr>
<th>Day</th>
<th>Activity</th>
</tr>
<tr>
<td>Monday</td>
<td>Soccer practice</td>
</tr>
<tr>
<td>Wednesday</td>
<td>Programming class</td>
</tr>
</table>
What’s new:
<table>— the table container.<tr>— table row.<th>— table header cell. Bold by default.<td>— table data cell.
Save. Reload. A table appears (probably without borders — we’ll style it next week).
⚠️ Don’t use tables for layout. Real-1990s websites used tables to position columns and rows of content. Modern web uses Flexbox (Session 5) for that. Tables are for actual tabular data only.
Semantic tags — what real pages use
In Session 1 you used <div> (or no wrapper) for page
sections. Modern HTML has specific tags for the
common parts of a page:
<header>— the top of the page (logo, title, banner).<nav>— navigation links (menu).<main>— the main content area.<section>— a thematic section of content.<article>— a self-contained piece (like a blog post).<aside>— sidebar / related links.<footer>— the bottom of the page (copyright, contact links).
These work exactly like <div> for the browser —
generic block containers. The difference: they
describe what they contain.
A real page structure
<!DOCTYPE html>
<html>
<head>
<title>Alex's homepage</title>
</head>
<body>
<header>
<h1>Alex's Homepage</h1>
<p>Welcome to my page!</p>
</header>
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="projects.html">Projects</a>
</nav>
<main>
<section>
<h2>Latest projects</h2>
<ul>
<li>Pong in Pygame</li>
<li>Fruit catcher</li>
</ul>
</section>
<section>
<h2>About me</h2>
<p>I'm learning to code.</p>
</section>
</main>
<footer>
<p>© 2026 Alex</p>
</footer>
</body>
</html>
Save. Reload. Looks the same as if you used <div>s
(no styling yet). But the structure is meaningful.
Why use semantic tags?
- Search engines understand them. Your site shows up better in Google.
- Screen readers (used by blind users) navigate by them.
- You and other developers can read the markup and instantly know what’s what.
Modern best practice: use semantic tags whenever they
fit. Use <div> only when nothing else does.
Checkpoint: You have a page using semantic tags
(at least <header>, <main>, <footer>) with
nested content. This is the natural stop point if
class is cut short.
Part B: Restructure your personal page
Take the personal page from Session 1 and restructure it with semantic tags.
What to do
-
Open your
index.htmlfrom last week. -
Wrap the title and intro in a
<header>. -
Add a
<nav>with at least one link (it can link back to itself for now — we’ll add real pages later). -
Wrap the main content (the sections, lists, etc.) in a
<main>. -
Wrap each major section in
<section>. -
Add a
<footer>at the bottom (copyright, your name, the year).
Add a navigation menu
A typical site has a nav menu at the top. Try:
<nav>
<ul>
<li><a href="#about">About</a></li>
<li><a href="#projects">Projects</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
The href="#about" is a fragment link — it scrolls
to an element with id="about" on the same page.
Add IDs to your sections:
<section id="about">
<h2>About me</h2>
...
</section>
Now clicking “About” in the nav scrolls to that section. Real navigation.
Stretch — make a multi-page site
Right now you have index.html. Add about.html and
projects.html:
<!-- about.html -->
<!DOCTYPE html>
<html>
<head><title>About</title></head>
<body>
<header>
<h1>About me</h1>
</header>
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="projects.html">Projects</a>
</nav>
<main>
<p>More about me here.</p>
</main>
</body>
</html>
Same skeleton, different content. Each page links to the others via the nav.
You now have a real multi-page website. Three files. Browser navigates between them. This is what most websites are.
Stretch — a project showcase table
Add a table for your Pygame projects:
<table>
<tr>
<th>Project</th>
<th>Phase</th>
<th>What it does</th>
</tr>
<tr>
<td>Snake</td>
<td>6</td>
<td>Classic snake game</td>
</tr>
<tr>
<td>Fruit catcher</td>
<td>6</td>
<td>Catch falling fruit</td>
</tr>
</table>
Extension — special characters
HTML “escapes” certain characters with &...; codes:
<—<(less-than)>—>(greater-than)&—&(ampersand)©— © (copyright)♥— ♥·— · (middle dot) — non-breaking space (a space that doesn’t break onto a new line)
Use them when you want a literal < (which would
otherwise look like a tag).
<p>The HTML for a heading is <h1>.</p>
Renders as: “The HTML for a heading is
.”
Extension — meta tags
In <head>, you can add meta tags that don’t show
on the page but tell browsers / search engines /
social media about it:
<head>
<title>Alex's homepage</title>
<meta charset="UTF-8">
<meta name="description" content="Alex's personal homepage with projects and stuff.">
<meta name="author" content="Alex">
</head>
Always include <meta charset="UTF-8"> — this tells
the browser to expect modern Unicode characters
(emoji, accented letters, etc.). Without it, weird
characters can render wrong.
Wrap-up
Before we leave, share with the room:
- For everyone — show your restructured page. Did it feel cleaner with semantic tags?
- For the kids who built multiple pages — show navigating between them.
- Did the table feel different from a list? When would you use one vs the other?
- Anyone discover a new tag they liked?
Today you learned:
- Nesting — tags inside tags. Indent for clarity.
- Block vs inline tags. Block stacks, inline flows.
<div>and<span>— generic containers.- Tables —
<table>,<tr>,<th>,<td>. For real tabular data. - Semantic tags —
<header>,<nav>,<main>,<section>,<article>,<aside>,<footer>. - Fragment links —
href="#section-id". - (Stretch) Multi-page sites with shared navigation.
- (Stretch) HTML entities —
©,&, etc. - (Stretch) Meta tags in
<head>.
Next week we add CSS — colors, fonts, spacing. Your page will start to look like a real website.
If you missed this session
Open Thonny.
-
Open your
index.htmlfrom Session 1 (or create a new one). -
Restructure with semantic tags —
<header>,<nav>,<main>,<section>s,<footer>. -
Add a navigation menu with at least 3 links.
-
(Stretch) Make a second page (
about.html) and link between them.
About 30-45 minutes. By the end you should have a properly structured page using semantic HTML.
Stretch and extension ideas
- Tables for tabular data — schedules, comparisons, lists of projects.
- Multi-page site with nav linking between pages.
- HTML entities for special characters.
- Meta tags in
<head>for descriptions. - Figure and figcaption:
<figure> <img src="cat.jpg" alt="My cat"> <figcaption>This is my cat, Whiskers.</figcaption> </figure> - Details and summary — collapsible sections:
Try it. The browser handles the show/hide automatically.<details> <summary>Click to reveal</summary> <p>Hidden content!</p> </details> - Read MDN’s HTML elements list. There are dozens. Skim them.
What’s next
Next week: CSS — adding style to your HTML. Colors, fonts, sizing, spacing. Your page will go from “raw HTML” to “looks like a website.”
Session 3: CSS basics — selectors, colors, typography
Session 3: CSS basics — selectors, colors, typography
Phase 7 — Web · Session 3 of 17
What we’re learning today
Your HTML page works. It looks like a 1995 textbook — all default fonts, plain blue links, no colors. Today we add CSS (Cascading Style Sheets) — the language that controls how things look. By the end you’ll have styled your personal page with colors, fonts, and text alignment, and it’ll feel like an actual website.
You’ll need to remember from last time
- HTML structure —
<header>,<main>,<section>,<footer>, etc. (Session 2). - Tags and attributes —
<a href="...">,<img src="...">. - Block vs inline distinction.
- The edit → save → reload loop.
Part A: Your first styles
What’s CSS?
CSS stands for Cascading Style Sheets. It’s a language separate from HTML for describing how HTML elements should look.
CSS doesn’t really look like a programming language — no loops, no conditions. It’s a list of rules:
selector {
property: value;
property: value;
}
A rule says: “elements that match this selector should have these properties.”
Example:
h1 {
color: blue;
font-size: 48px;
}
Reads as: “Every <h1> element should be blue, with
font size 48 pixels.”
Three places to put CSS
CSS can live in three places. From “good for tiny quick changes” to “good for real sites”:
1. Inline (worst — avoid for real work):
<h1 style="color: blue;">Hello</h1>
The style="..." attribute. Quick, but each element
needs its own copy. Mess.
2. Internal (in <style> block in <head>):
<head>
<style>
h1 {
color: blue;
}
</style>
</head>
All styles in one place per page. OK for small pages.
3. External (separate .css file — best):
<head>
<link rel="stylesheet" href="styles.css">
</head>
CSS lives in styles.css. Multiple pages can share
the same stylesheet. This is what real sites use.
Use this from now on.
Make your first CSS file
Open Thonny. In your project folder (next to your
index.html), create a new file. Save as
styles.css. Type:
h1 {
color: darkred;
font-family: Arial, sans-serif;
}
p {
color: #333;
font-size: 18px;
line-height: 1.5;
}
Now in index.html, add the link in <head>:
<head>
<title>My page</title>
<link rel="stylesheet" href="styles.css">
</head>
Save both files. Reload your index.html in the
browser. Your headings are dark red, paragraphs are
dark grey, and everything’s in Arial.
You styled a page. You’re a CSS developer now.
What changed
color: darkred;— text color.font-family: Arial, sans-serif;— try Arial first, fall back to any sans-serif font if Arial isn’t available.color: #333;— a hex color code (more on this below).font-size: 18px;— text size in pixels.line-height: 1.5;— space between lines (1.5 = 1.5× the font size). Great for readability.
Colors
CSS supports several color formats. The common ones:
Named colors — about 140 of them: red, blue,
green, pink, darkred, tomato, dodgerblue,
mediumseagreen, cornflowerblue, etc. Find them all
on MDN.
h1 { color: tomato; }
Hex codes — 6-digit #rrggbb (red, green, blue
in hex):
h1 { color: #ff5050; } /* tomato-ish */
h1 { color: #333; } /* dark grey */
h1 { color: #fff; } /* white */
h1 { color: #000; } /* black */
(#fff is short for #ffffff — when each pair has
the same two digits, you can write just one.)
RGB — same as Pygame:
h1 { color: rgb(255, 80, 80); }
RGBA — RGB plus alpha (transparency 0-1):
h1 { color: rgba(255, 80, 80, 0.5); } /* 50% transparent */
Pick whichever feels comfortable. Hex is most common in real CSS.
Selectors
A selector says which elements get the rule. So far you’ve used element selectors — by tag name. There are more:
Element selector — by tag:
h1 { color: red; } /* every <h1> */
p { color: blue; } /* every <p> */
Class selector — by class attribute (note the dot):
.callout {
background-color: yellow;
padding: 10px;
}
In HTML:
<p class="callout">This stands out.</p>
<div class="callout">So does this.</div>
Both elements get the styling because they have
class="callout". Classes are how you style
specific things.
ID selector — by id attribute (note the hash):
#main-title {
font-size: 60px;
}
In HTML:
<h1 id="main-title">Hi!</h1>
IDs should be unique per page — only one element
can have a given id. Classes can be used many times.
Rule of thumb: use classes for styling. IDs are more for JavaScript and fragment links (Session 2).
Combine selectors
h1, h2, h3 {
font-family: Georgia, serif;
}
Comma-separated selectors apply the same rules to multiple element types.
header h1 {
color: white;
}
Space-separated means descendants — “every <h1>
that’s inside a <header>.”
.card h2 {
color: navy;
}
“Every <h2> inside an element with class card.”
That’s the start of CSS’s power — targeting elements based on context.
The cascade — last rule wins
When two rules apply to the same element and conflict, the later one wins (with some exceptions for specificity, see below):
h1 { color: red; }
h1 { color: blue; }
Result: <h1> is blue.
This is what “Cascading” in “Cascading Style Sheets” means — rules cascade down, later overrides earlier.
Specificity (briefly)
When two rules conflict, more specific selectors win:
- Element selectors are weakest.
- Class selectors are stronger.
- ID selectors are stronger still.
p { color: blue; } /* every <p> */
.special { color: red; } /* <p class="special"> wins */
#unique { color: green; } /* <p id="unique"> wins over both */
For now: prefer classes for everything. Don’t fight specificity wars; they’re confusing and lead to bad CSS.
Style your personal page
Open index.html and styles.css from earlier
sessions. Add some real styling. Try:
body {
font-family: Georgia, serif;
background-color: #f4f1eb;
color: #333;
max-width: 700px;
margin: 40px auto;
padding: 20px;
}
header {
text-align: center;
border-bottom: 2px solid #ccc;
padding-bottom: 20px;
}
h1 {
color: #2c3e50;
font-size: 42px;
}
h2 {
color: #c0392b;
font-size: 28px;
}
a {
color: #2980b9;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
Save. Reload. Your page now looks designed — a neutral background, centered content, clear typography, links that aren’t default-blue.
A few new things in there:
body— styles the whole page.max-width: 700px;— limits the page width (otherwise text spans the whole window — hard to read).margin: 40px auto;— vertical margin 40px, horizontalauto(centers the page horizontally).text-align: center;— centers text.border-bottom: 2px solid #ccc;— a thin grey line under the header.text-decoration: none;— removes the default underline on links.a:hover— a pseudo-class that applies only when the mouse is over the link. Restores underline on hover.
Checkpoint: Your personal page has a custom font, custom colors, and a max-width with centered layout. This is the natural stop point if class is cut short.
Part B: Make it look like yours
The CSS above is a starting point. Now make it yours. Pick a personality — bright and friendly, dark and dramatic, retro and pixel-y, minimal and clean. Then style accordingly.
Try different fonts
CSS includes only the fonts installed on the user’s system. Common safe ones:
Arial— sans-serif, classicHelvetica— sans-serif, clean (Mac)Georgia— serif, friendlyTimes New Roman— serif, classicalCourier New— monospaceComic Sans MS— yes, this existsVerdana— sans-serif, web-friendly
Always include a fallback:
body { font-family: Georgia, "Times New Roman", serif; }
Web fonts (stretch)
For real font variety, use Google Fonts — free fonts hosted on Google’s CDN. Example:
<head>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&family=Lobster&display=swap">
<link rel="stylesheet" href="styles.css">
</head>
body { font-family: 'Inter', sans-serif; }
h1 { font-family: 'Lobster', cursive; }
fonts.google.com lets you pick any font and gives you the link.
Build a color palette
Don’t pick colors at random — pick a palette of 3-5 that work together. Resources:
- coolors.co — generate palettes.
- color.adobe.com — Adobe’s color tool.
Apply consistently across your page.
Stretch — try other properties
Each of these is one CSS property to play with:
text-align: left | center | right | justify;— text alignment.font-weight: bold | normal | 400 | 700;— bold or not.font-style: italic | normal;— italic.text-transform: uppercase | lowercase | capitalize;letter-spacing: 2px;— between letters.background-color: ...;— element background.background-image: url('image.jpg');— image background.opacity: 0.7;— semi-transparent.
Look up any of them on MDN.
Stretch — pseudo-classes
We saw :hover. Others:
a:visited— already-visited links.button:active— while being clicked.input:focus— when the user clicked into the field.p:first-child— first paragraph in its parent.li:nth-child(odd)— every other list item (alternating colors!).
li:nth-child(even) {
background-color: #f0f0f0;
}
Alternating list-item backgrounds. Looks great.
Extension — DevTools inspector
In your browser, right-click any element → Inspect. A panel appears showing the HTML and the CSS that’s applied. You can change CSS values live to experiment.
Try it on your page. Try it on a real website. This is how real web developers work — inspect, tweak, copy the changes back to your file.
Ctrl+Shift+I (or Cmd+Opt+I) opens DevTools too.
Wrap-up
Before we leave, share with the room:
- For everyone — show your styled page. What personality did you go for?
- Did anyone use Google Fonts? Show the result.
- Anyone use DevTools? Did it feel powerful?
- Did the cascade ever bite you? (Two rules conflicted and you couldn’t figure out which won?)
Today you learned:
- CSS is the styling language for the web.
- External
.cssfiles linked from<head>are the standard. - Selectors: element (
h1), class (.callout), ID (#main), descendant (.card h2). - Properties:
color,background-color,font-family,font-size,text-align,line-height,margin,padding,border. - Color formats: named (
red), hex (#ff5050), RGB (rgb(...)), RGBA (with alpha). - The cascade: later rules win; specificity matters.
- Pseudo-classes:
:hover,:focus,:nth-child. - DevTools inspector for live experimentation.
Next week we go deeper into the box model — margin, padding, border. The math of space around things.
If you missed this session
Open Thonny.
-
Create a
styles.cssfile next to yourindex.html. -
Link it in
<head>:<link rel="stylesheet" href="styles.css">. -
Style at least: body, h1, h2, p, a. Try colors, fonts, and at least one
:hover. -
Save both. Reload
index.html.
About 30-45 minutes. By the end your page should look designed, not raw.
Stretch and extension ideas
- Build a color palette with a tool. Apply across your page.
- Google Fonts for font variety.
- More pseudo-classes —
:nth-child,:first-of-type. - CSS variables — define colors once, use
everywhere:
:root { --primary: #2c3e50; --accent: #c0392b; } h1 { color: var(--primary); } - Comments in CSS —
/* this is a comment */. - Transitions — animate on hover:
a { color: navy; transition: color 0.3s; } a:hover { color: red; }
What’s next
Next week: the box model — every element on a page is a rectangular box. Understanding margin, padding, border, and how they combine is the key to controlling space and layout.
Session 4: The box model
Session 4: The box model
Phase 7 — Web · Session 4 of 17
What we’re learning today
Every element on a page is a rectangular box. Headings, paragraphs, images, buttons, even text inside a span — all boxes. Today we learn the anatomy of those boxes (content, padding, border, margin) and how to control the space around things. Understanding the box model is the foundation of every layout you’ll ever build.
You’ll need to remember from last time
- CSS basics — selectors, properties, the cascade.
- External stylesheet linked from
<head>. - Block vs inline elements (Session 2).
- Setting properties with
selector { prop: val; }.
Part A: The box model
Every element is a box
Open your styled page from Session 3. Right-click any element → Inspect. Look at the Box Model panel (usually on the right side of DevTools). You see four nested rectangles:
┌─────────────────── margin ───────────────────┐
│ │
│ ┌─────────── border ────────────┐ │
│ │ │ │
│ │ ┌────── padding ──────┐ │ │
│ │ │ │ │ │
│ │ │ [ content ] │ │ │
│ │ │ │ │ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
From inside out:
- Content — the text or image itself.
- Padding — space inside the border, between the border and the content.
- Border — a line around the padding.
- Margin — space outside the border, between this element and others.
That’s the box model.
The mental model:
- Padding = “space inside me, around my content.”
- Border = “a line at my edge.”
- Margin = “space outside me, pushing other things away.”
Try it
In your styles.css, add:
.demo-box {
background-color: lightblue;
border: 5px solid darkblue;
padding: 20px;
margin: 30px;
}
In index.html:
<div class="demo-box">
This is the content.
</div>
Save. Reload. You see a blue box with content inside, a thick blue border, lots of empty space inside (the padding), and lots of empty space around it (the margin).
Open DevTools → Inspect the box. The Box Model panel shows the numbers you set.
Modify each one
Change values one at a time:
padding: 50px;— content moves further from the border.border: 10px dashed red;— fatter, dashed, red.margin: 100px;— pushes other things away from this box.background-color: lightyellow;— changes inside.
Reload after each. Watch how the box changes.
Padding shorthand — top, right, bottom, left
padding: 20px; /* all four sides */
padding: 10px 20px; /* top/bottom 10, left/right 20 */
padding: 10px 20px 30px; /* top, sides, bottom */
padding: 10px 20px 30px 40px; /* top, right, bottom, left (clockwise) */
Or specify each side:
padding-top: 10px;
padding-right: 20px;
padding-bottom: 30px;
padding-left: 40px;
Same for margin — same shorthand and same individual
properties (margin-top, etc.).
Border shorthand
border: 2px solid black;
Three values: width, style, color.
Styles include solid, dashed, dotted, double,
groove, ridge. (solid is most common.)
You can also do individual sides:
border-bottom: 2px solid #ccc; /* only bottom */
We used this in Session 3 for the divider under the header.
Width and height
By default, block elements take up the full width of their container, and their height adjusts to fit content.
.box {
width: 300px;
height: 200px;
}
Now the box is exactly 300×200 pixels.
But here’s the gotcha: width is the content width, not the total box width. If you set:
.box {
width: 300px;
padding: 20px;
border: 5px solid black;
}
The total width is 350px (300 + 20 + 20 + 5 + 5).
This is the original CSS behavior — confusing and annoying. So everyone uses the modern fix:
box-sizing: border-box — the modern default
*, *::before, *::after {
box-sizing: border-box;
}
Add this at the top of your styles.css. It
applies to every element on the page (* is the
universal selector).
Now width: 300px; means total width, including
padding and border. Way more intuitive.
Every modern site uses box-sizing: border-box.
Set it once, never think about it again.
Display: block, inline, inline-block
Briefly, every element has a display property:
block— full width, stacks vertically. Default for<div>,<p>,<h1>, etc.inline— only as wide as its content, flows with text. Default for<span>,<a>,<strong>. Padding and margin sometimes don’t work as expected on inline elements (vertical margins are ignored).inline-block— flows like inline, but accepts width/height/margin like block. Useful sometimes.flex— Session 5.none— element disappears entirely from the page (not just hidden — not rendered).
You can override the default:
a.button {
display: inline-block;
padding: 10px 20px;
background-color: navy;
color: white;
}
Now an <a class="button"> looks and behaves like a
real button.
Checkpoint: You’ve changed padding, margin, and border on at least one element, and you understand the difference between padding (inside) and margin (outside). This is the natural stop point if class is cut short.
Part B: Card layouts
A common pattern on real sites: cards — boxes with content inside, separated by margin, with padding inside and a subtle border or shadow.
Build it
Add to your HTML (inside <main>):
<section class="card-row">
<article class="card">
<h3>Pong</h3>
<p>My first complete game from Phase 6. Two paddles, one ball.</p>
<a href="https://github.com/...">See on GitHub</a>
</article>
<article class="card">
<h3>Fruit Catcher</h3>
<p>Catch falling fruit. Don't catch bombs.</p>
<a href="https://github.com/...">See on GitHub</a>
</article>
<article class="card">
<h3>Todo App</h3>
<p>My customtkinter project. Add tasks, save to disk.</p>
<a href="https://github.com/...">See on GitHub</a>
</article>
</section>
Add to your CSS:
*, *::before, *::after {
box-sizing: border-box;
}
.card {
background-color: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.card h3 {
margin-top: 0;
color: #2c3e50;
}
.card a {
display: inline-block;
margin-top: 10px;
background-color: #3498db;
color: white;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
}
.card a:hover {
background-color: #2980b9;
}
Save. Reload. You have project cards stacked vertically, each one with subtle shadow, rounded corners, padding, and a styled button-link.
What’s new:
border-radius: 8px;— rounded corners. (50%on a square = circle.)box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);— subtle drop shadow. Format:x-offset y-offset blur-radius color.margin-top: 0;— kills the default top margin on the<h3>(browsers add default margins on headings; sometimes you don’t want them).display: inline-block;on the link — makes the link sized like a real button.
Cards stacking vertically is fine — but they’d look better side-by-side. That’s Flexbox, next session.
Stretch — try other shadows and borders
/* heavier shadow */
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
/* shadow on hover */
.card { transition: box-shadow 0.2s; }
.card:hover { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); }
/* dashed border */
border: 2px dashed #999;
/* gradient background */
background: linear-gradient(to right, #6a11cb, #2575fc);
Stretch — .btn class
Most sites have a button class used everywhere. Generalize the link styling:
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
border: none;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #95a5a6;
}
.btn-secondary:hover {
background-color: #7f8c8d;
}
Now any <a class="btn">, <button class="btn">, or
<a class="btn btn-secondary"> gets the right look.
cursor: pointer; makes the cursor a hand on hover —
clue for users that something’s clickable.
Extension — DevTools box-model panel deep dive
Open DevTools. Inspect a card. Look at the Computed tab and Layout tab.
The Box Model panel shows your card’s exact margin, border, padding, and content sizes in pixels. Try changing padding directly in DevTools — see the box resize live.
This is the #1 way to debug layout issues in real web work.
Wrap-up
Before we leave, share with the room:
- For everyone — show your card layout. How many cards did you add?
- Did the box model click? Can you tell where each box starts and ends?
- For the kids who used DevTools — does it feel like X-ray vision into the page?
- Anyone discover
border-radius: 50%(makes circles)?
Today you learned:
- Every element is a box with content, padding, border, margin.
- Padding = inside. Margin = outside. Border = the line at the edge.
- Shorthand:
padding: 10px 20px;and similar. box-sizing: border-box;— modern default, always set it.- Width and height — by default, width is content
only; with
border-box, width includes padding and border. - Display:
block(stack),inline(flow),inline-block(flow + sizing),none(gone). border-radiusfor rounded corners.box-shadowfor shadows.
Cards are everywhere on the modern web. You can build them.
Next week: Flexbox — finally, real layout. Cards side-by-side. Header-content-footer. Centering things properly.
If you missed this session
Open Thonny.
-
Open your
index.htmlandstyles.cssfrom Session 3. -
Add
*, *::before, *::after { box-sizing: border-box; }at the top ofstyles.css. -
Pick one element (a paragraph, the header). Try adding padding (inside space), margin (outside space), border (the edge line). Reload after each.
-
Build the card layout from Part B with at least 3 cards.
About 30-45 minutes. By the end you should have a page with cards.
Stretch and extension ideas
- Card hover effect — change shadow on hover.
- Different card colors — use a class like
.card.featured { background: yellow; }. .btnreusable class for buttons across your site.border-radius: 50%— circles on any square element.box-shadowvariations — soft, hard, multiple shadows.transition— smooth animations on hover.- CSS variables for spacing:
:root { --space-sm: 8px; --space-md: 16px; --space-lg: 32px; } .card { padding: var(--space-md); }
What’s next
Next week: Flexbox — modern layout. Cards side-by-side. Navbars. Headers and footers properly positioned. The single most useful layout system in modern CSS.
Session 5: Flexbox layout
Session 5: Flexbox layout
Phase 7 — Web · Session 5 of 17
What we’re learning today
Boxes by default stack vertically — each one on its own line. To put boxes side-by-side, or to center something properly, you need a layout system. Flexbox is the modern answer. By the end of class you’ll have built side-by-side cards, a navbar with logo + links, and a header-content-footer layout. This is real layout — the kind every modern site uses.
You’ll need to remember from last time
- The box model — padding, margin, border.
box-sizing: border-box;at the top of your CSS.- Display values — block, inline, inline-block.
- Card pattern from Session 4.
Part A: The Flexbox basics
What’s Flexbox?
Flexbox is a CSS layout system designed for arranging items in a row or column, with smart control over spacing, alignment, and sizing.
The key shift in mental model:
- Without Flexbox: every block element gets its own line. Each is on a row by itself.
- With Flexbox: you can say “these things go in a row, evenly spaced, vertically centered.”
The basic recipe
Flexbox needs a container with display: flex;,
and the direct children become flex items.
<div class="row">
<div class="card">A</div>
<div class="card">B</div>
<div class="card">C</div>
</div>
.row {
display: flex;
}
That’s it. The three .cards now sit in a row
instead of stacking.
Try it. Open your index.html. Add this somewhere in
<main>:
<div class="row">
<div class="box">A</div>
<div class="box">B</div>
<div class="box">C</div>
</div>
Add CSS:
.row {
display: flex;
}
.box {
background-color: lightblue;
padding: 30px;
margin: 5px;
border: 2px solid darkblue;
}
Save. Reload. Three boxes in a row. No floats, no
positioning, no tables. Just display: flex;.
Now remove display: flex; from .row. Reload.
Three boxes stacked vertically (back to default).
That one rule is the difference. Pretty powerful.
Direction — row or column
By default, Flexbox arranges children in a row. You can switch to a column:
.row {
display: flex;
flex-direction: column;
}
Now items stack vertically — but with all of Flexbox’s other powers.
The two values you’ll use 99% of the time: row
(default) and column.
gap — spacing between items
Want space between items? Use gap:
.row {
display: flex;
gap: 20px;
}
20-pixel gap between each item, automatically. You no longer need margin-right on each item or any of the old hacks.
Add gap to your .row. Watch the boxes get evenly
spaced.
justify-content — main axis alignment
The main axis is the direction of flex-direction
— row by default, so the main axis is horizontal.
justify-content controls alignment along the main
axis.
The values:
flex-start(default) — items at the start (left, in a row).flex-end— items at the end (right).center— items centered horizontally.space-between— items spread out, no space on the edges.space-around— items spread out, equal space around each.space-evenly— items spread out, equal space between and around.
.row {
display: flex;
justify-content: space-between;
}
Try each. Watch the items reposition.
align-items — cross axis alignment
The cross axis is perpendicular to the main axis
— for a row, it’s vertical. align-items controls
alignment along the cross axis.
The values:
stretch(default) — items fill the container’s height.flex-start— items at the top.flex-end— items at the bottom.center— items vertically centered.
This is the #1 reason most people start using Flexbox: vertical centering used to be hard. Now it’s one rule:
.row {
display: flex;
align-items: center;
}
If the row is taller than its items (because of, say,
a min-height), the items center vertically inside.
Centering anything (the classic trick)
.center-everything {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
background-color: #eee;
}
<div class="center-everything">
<p>I'm centered both ways!</p>
</div>
The <p> ends up dead center, both horizontally and
vertically. The classic problem of CSS, solved in
three lines.
Checkpoint: You can put items in a row, change
direction, add gaps, and align them with
justify-content and align-items. This is the
natural stop point if class is cut short.
Part B: Real layouts
Time to use Flexbox for the things real sites do.
A navbar (logo + links)
Most sites have a navbar with a logo on the left and links on the right. Flexbox does this perfectly.
<header class="site-header">
<div class="logo">My Site</div>
<nav>
<a href="index.html">Home</a>
<a href="about.html">About</a>
<a href="projects.html">Projects</a>
</nav>
</header>
.site-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 32px;
background-color: #2c3e50;
color: white;
}
.logo {
font-size: 24px;
font-weight: bold;
}
.site-header nav {
display: flex;
gap: 24px;
}
.site-header a {
color: white;
text-decoration: none;
}
.site-header a:hover {
color: #3498db;
}
Save. Reload. A real navbar. Logo on the left, links on the right, all vertically centered, evenly spaced, hoverable.
The two Flexbox containers:
- The header itself: row with logo on one end and nav
on the other (
justify-content: space-between). - The nav: row of links with
gapbetween them.
Cards in a row
Take the cards from Session 4. Wrap them in a flex row:
<section class="card-row">
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
</section>
.card-row {
display: flex;
gap: 20px;
}
.card {
flex: 1; /* each card grows equally */
}
Save. Reload. Three cards side-by-side, each taking equal width.
The flex: 1 property is shorthand for “this item
should grow to fill available space, sharing equally
with siblings.” Three cards with flex: 1 get equal
thirds. Two cards with flex: 1 get equal halves.
You can also set flex to other numbers:
.card-main { flex: 2; } /* twice as wide */
.card-side { flex: 1; }
Header-content-footer (a column layout)
A site with a sticky header, expanding content area, and footer at the bottom:
<body>
<header class="site-header">...</header>
<main class="site-content">...</main>
<footer class="site-footer">...</footer>
</body>
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.site-content {
flex: 1; /* take all available space */
}
Now the body is a vertical flex column. Header at top, footer at bottom, content fills everything in between. Even when the page is mostly empty, the footer sits at the bottom of the screen.
100vh = 100% of the viewport height (the browser
window). The body is at least that tall.
Stretch — wrapping for many cards
If you have many cards (more than fit in one row),
they overflow. Solution: flex-wrap:
.card-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.card {
flex: 1 1 250px; /* grow, shrink, base width */
}
Now cards wrap to the next line as needed. Each card is at least 250px wide.
flex: 1 1 250px; is the long form of flex. Three
parts:
- flex-grow — how much to grow (1 = grow).
- flex-shrink — how much to shrink (1 = shrink if needed).
- flex-basis — base width (250px).
This is responsive design — the layout adapts to screen size.
Stretch — a sidebar layout
Two-column layout: sidebar on the left, content on the right:
<div class="layout">
<aside class="sidebar">Sidebar</aside>
<main class="main-content">Main content</main>
</div>
.layout {
display: flex;
gap: 20px;
}
.sidebar {
flex: 0 0 200px; /* don't grow, don't shrink, 200px wide */
}
.main-content {
flex: 1; /* fill the rest */
}
Sidebar is fixed at 200px; main content takes the rest. Real sites use this everywhere.
Extension — restyle your homepage
Take your index.html from previous sessions and use
Flexbox to:
- Add a navbar at the top with logo + links.
- Put your project cards in a row (use
flex-wrap). - Make the footer stick to the bottom.
By the end, your page looks like a real website — navigated, card-laid-out, footer-anchored.
(Next week we make it the full personal homepage as a complete integration project.)
Wrap-up
Before we leave, share with the room:
- For everyone — show your Flexbox layout. What did you build?
- Did vertical centering finally work? (Anyone who tried web stuff before today knows the pain.)
- For the kids who tried
flex-wrap— does the responsive behavior feel modern? - What clicked first:
justify-contentoralign-items?
Today you learned:
- Flexbox = modern layout for rows and columns.
display: flex;on the parent.flex-direction: row | column;sets the main axis.gapfor spacing between items.justify-contentfor main-axis alignment.align-itemsfor cross-axis alignment.flex: 1to make items grow equally.flex-wrapfor items overflowing to new rows.100vhfor full-viewport-height.
Flexbox is the most useful CSS feature for day-to-day work. Almost every layout you’ll build for the rest of the curriculum (and beyond) uses it.
Next week: a personal homepage — your integration project. HTML + CSS + Flexbox combined into one polished site.
If you missed this session
Open Thonny.
-
Add the basic flex example to your page (a
.rowwith three boxes inside). -
Try
justify-content: centerandalign-items: center. -
Convert your project cards (Session 4) to a flex row using
display: flexandflex: 1. -
(Stretch) Add a navbar with
justify-content: space-between.
About 45-60 minutes. By the end you should have at least two flex layouts working.
Stretch and extension ideas
- Responsive cards with
flex-wrap. - Two-column layout with sidebar.
- Sticky footer with column flex on body.
- Nested flex — a flex inside a flex (totally fine, common).
- Visual reference: flexboxfroggy.com — a game for practicing Flexbox. Genuinely fun.
align-selfon individual items to overridealign-itemsfor that one item.orderon items to rearrange visually without changing HTML order.justify-content: space-betweenpatterns — navbars are the canonical use.
What’s next
Next week: build a personal homepage. HTML + CSS
- Flexbox combined. Your real first website.
Session 6: Build a personal homepage
Session 6: Build a personal homepage
Phase 7 — Web · Session 6 of 17
What we’re learning today
You have HTML, CSS, the box model, Flexbox. Today we put it all together and build your real personal homepage — the one that’ll go on GitHub Pages in Session 15 with a public URL anyone can visit. We build it step by step. By the end you’ll have a polished site you’d be proud to share.
You’ll need to remember from last time
- HTML structure — semantic tags (Session 2).
- CSS — selectors, colors, fonts (Session 3).
- The box model — padding, margin, border (Session 4).
- Flexbox —
display: flex,justify-content,align-items,gap,flex: 1(Session 5). box-sizing: border-boxat the top of every stylesheet.
Part A: Plan and build the structure
What’s a personal homepage?
A page about you. What you make, what you like, who you are. Modern professionals (developers, artists, writers) all have one. Yours has these jobs:
- Tell visitors who you are.
- Show what you’ve made (Phase 6 games, Phase 5 apps, anything you’re proud of).
- Be visually interesting without being chaotic.
We’ll build a single-page version today. (You can expand to multiple pages later.)
Sketch first
Take 5 minutes. On paper, sketch what your page should look like. Think about:
- Top: a navbar (logo + links) and/or a big hero area with your name.
- Middle: sections — about you, projects, hobbies, whatever.
- Bottom: a footer (copyright, contact links).
Most pages roughly follow this shape:
┌─────────────────────────┐
│ navbar (logo + nav) │
├─────────────────────────┤
│ │
│ hero (name + │
│ tagline + intro) │
│ │
├─────────────────────────┤
│ │
│ about section │
│ │
├─────────────────────────┤
│ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │card│ │card│ │card│ projects
│ └────┘ └────┘ └────┘ │
│ │
├─────────────────────────┤
│ footer │
└─────────────────────────┘
Step 1 — Skeleton
Open Thonny. Create a new folder for your homepage if
you want. Create index.html and styles.css inside
it.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Alex's Homepage</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="navbar">
<div class="logo">Alex</div>
<nav>
<a href="#about">About</a>
<a href="#projects">Projects</a>
<a href="#contact">Contact</a>
</nav>
</header>
<section class="hero">
<h1>Hi, I'm Alex.</h1>
<p class="tagline">Programmer, gamer, future game dev.</p>
</section>
<main>
<section id="about" class="content-section">
<h2>About me</h2>
<p>I'm 12 years old. I'm in From Scratch Programming Class. I've shipped six projects so far.</p>
</section>
<section id="projects" class="content-section">
<h2>Projects</h2>
<div class="card-row">
<article class="card">
<h3>Pong</h3>
<p>My first complete game. Two paddles, one ball.</p>
<a href="#" class="btn">See on GitHub</a>
</article>
<article class="card">
<h3>Fruit Catcher</h3>
<p>Catch fruit. Don't catch bombs.</p>
<a href="#" class="btn">See on GitHub</a>
</article>
<article class="card">
<h3>Todo App</h3>
<p>My customtkinter project — add tasks, save to disk.</p>
<a href="#" class="btn">See on GitHub</a>
</article>
</div>
</section>
<section id="contact" class="content-section">
<h2>Contact</h2>
<p>Find me on <a href="https://github.com/...">GitHub</a>.</p>
</section>
</main>
<footer>
<p>© 2026 Alex</p>
</footer>
</body>
</html>
Save. Open in browser. Looks plain — that’s expected. CSS is next.
Step 2 — Reset and base styles
styles.css:
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', -apple-system, sans-serif;
color: #333;
background-color: #f7f7f7;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
Save. Reload. The page now has:
- The universal
box-sizing(set once, forever). margin: 0on body kills the default 8px browser margin.- A clean font stack.
- The body is a flex column, so the footer will stick to the bottom (Session 5 pattern).
Step 3 — The navbar
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 32px;
background-color: #2c3e50;
color: white;
}
.logo {
font-size: 24px;
font-weight: bold;
}
.navbar nav {
display: flex;
gap: 24px;
}
.navbar a {
color: white;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.navbar a:hover {
color: #3498db;
}
Save. Reload. Real navbar — logo on the left, nav links on the right, dark background, hover effect.
Step 4 — Hero section
.hero {
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
color: white;
padding: 80px 32px;
text-align: center;
}
.hero h1 {
font-size: 56px;
margin: 0 0 16px 0;
}
.hero .tagline {
font-size: 24px;
opacity: 0.9;
margin: 0;
}
Save. Reload. A hero section with a gradient background and big centered text. Looks like a real modern site.
linear-gradient(135deg, color1, color2) makes a
diagonal gradient between two colors. Try other angles
(90deg, 180deg, to right) and other colors.
Step 5 — Content sections
.content-section {
max-width: 1000px;
margin: 0 auto;
padding: 60px 32px;
}
.content-section h2 {
font-size: 36px;
color: #2c3e50;
margin-top: 0;
border-bottom: 3px solid #3498db;
padding-bottom: 8px;
margin-bottom: 32px;
}
Save. Reload. Each section has good spacing, max width that keeps text readable, and a styled heading with an accent underline.
Step 6 — Cards
.card-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.card {
flex: 1 1 250px; /* grow, shrink, base width */
background-color: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.card h3 {
margin-top: 0;
color: #2c3e50;
}
.btn {
display: inline-block;
padding: 8px 16px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 4px;
font-weight: 500;
transition: background-color 0.2s;
}
.btn:hover {
background-color: #2980b9;
}
Save. Reload. Project cards in a row, with a satisfying hover lift.
The transform: translateY(-4px) on hover lifts the
card up 4 pixels. Combined with the bigger shadow,
it feels like the card is rising. Real-modern-web
polish.
flex: 1 1 250px; means: grow to fill, shrink if
needed, with a base of 250px. On wide screens the
cards spread out; on narrower screens they wrap.
Step 7 — Footer
footer {
background-color: #2c3e50;
color: white;
text-align: center;
padding: 24px;
margin-top: 60px;
}
Save. Reload. Done. A complete, polished personal homepage.
Checkpoint: Your homepage has a navbar, hero section, about, project cards, and footer — all styled. This is the natural stop point if class is cut short.
Part B: Make it yours
The structure is the same for everyone. The content should not be.
Real content
Replace every word with yours:
- The name in the navbar/hero.
- The tagline (one sentence about you — interest, hobby, vibe).
- About paragraph(s).
- Project cards — your actual Phase 6 projects, their real GitHub URLs (if you’ve pushed), your real descriptions.
- Footer — your real name and year.
Don’t leave any “Alex” or “lorem ipsum.” Make every word real.
Pick a personality
Customize colors. Pick 2-3 that work together:
/* Calm — blue/grey */
--primary: #2c3e50;
--accent: #3498db;
--bg: #f7f7f7;
/* Warm — orange/terra */
--primary: #6b3838;
--accent: #d35400;
--bg: #fdf6e3;
/* Dark — modern dev vibe */
--primary: #1a1a2e;
--accent: #e94560;
--bg: #0f3460;
--text: #f0f0f0;
/* Soft — pastel */
--primary: #6c5b7b;
--accent: #c06c84;
--bg: #f8e7e7;
Then in CSS:
:root {
--primary: #2c3e50;
--accent: #3498db;
--bg: #f7f7f7;
}
body { background-color: var(--bg); }
.navbar { background-color: var(--primary); }
.btn { background-color: var(--accent); }
:root defines CSS variables. var(--name) uses
them. Change one variable, the whole page changes.
Real images
Find or take real images:
- A photo (or avatar / cartoon you).
- Screenshots of your Pygame projects.
- Hobby photos.
Add <img src="..." alt="..."> where it fits. Style:
img {
max-width: 100%; /* never wider than its container */
border-radius: 8px;
}
.hero img {
width: 200px;
height: 200px;
border-radius: 50%; /* circle */
border: 4px solid white;
}
Stretch — multi-page
Add real about.html and projects.html pages.
Update the nav to link to them. Each page has the
same navbar (copy-paste). Real multi-page site.
Stretch — favicon
A favicon is the tiny icon in the browser tab. Find a
small PNG (or use one of your sprite assets), save as
favicon.png in your folder, and add to <head>:
<link rel="icon" type="image/png" href="favicon.png">
Reload. Your icon shows in the tab. Tiny detail, big “real site” feeling.
Extension — responsive design
Real sites adjust to phone screens. You can add this:
@media (max-width: 600px) {
.navbar {
flex-direction: column;
gap: 12px;
}
.hero h1 {
font-size: 36px;
}
.content-section {
padding: 40px 16px;
}
}
@media (max-width: 600px) says “these rules apply
only when the screen is 600 pixels or narrower.”
Resize your browser window to test.
This is mobile-first / responsive design. A real modern web skill.
Wrap-up
Before we leave, share with the room:
- For everyone — show your homepage. Tour us through it.
- What’s the personality you went for?
- For the kids who used CSS variables — how did the one-place-changes-everything feel?
- For the kids who tested at narrow widths — does it look OK on phone-ish screens?
Today you built a real, polished website. Not a toy. The kind of thing you could actually share with people. (And you will — Session 15 puts it on a public URL.)
You used:
- Semantic HTML (
<header>,<main>,<section>,<footer>). - External CSS with
box-sizing: border-box. - Flexbox for navbar and card layouts.
- The box model — padding, margin, borders, shadows.
- Custom typography and colors — your style.
- Hover transitions — real polish.
Next week we add forms — text inputs, checkboxes, buttons that look right. Then comes JavaScript.
If you missed this session
Open Thonny.
-
Create a folder for your homepage. Create
index.htmlandstyles.css. -
Type the HTML structure (navbar, hero, sections, footer) from Part A.
-
Type the CSS in steps 2-7. Reload after each step.
-
Customize content and colors to be yours.
About 60-90 minutes — this is a long, integrated session.
Stretch and extension ideas
- CSS variables for a centralized color palette.
- Multi-page site — about.html, projects.html.
- Favicon in the browser tab.
- Responsive design with
@mediaqueries. - Smooth scroll on nav clicks:
html { scroll-behavior: smooth; } - Sticky navbar — stays at the top while
scrolling:
.navbar { position: sticky; top: 0; z-index: 10; } - Animations — subtle fade-in on scroll. Advanced (uses JS), but a stretch.
- Real screenshots of your projects in the cards.
What’s next
Next week: forms — input fields, text areas, checkboxes, dropdowns, buttons. The web’s input controls. Then we tackle JavaScript and make everything do things.
Session 7: Forms — inputs, labels, buttons
Session 7: Forms — inputs, labels, buttons
Phase 7 — Web · Session 7 of 17
What we’re learning today
Forms are how users send information to a website — sign-ups, searches, contact forms, comments, polls. Today you learn the form-related HTML tags (input, label, textarea, select, button) and how to style them. By the end you’ll have built a contact form that looks right. Next sessions (after we learn JavaScript) we’ll make it actually do things.
You’ll need to remember from last time
- HTML basics — tags, attributes.
- CSS basics — selectors, properties.
- The box model — padding, margin, border.
- Flexbox —
display: flex,gap,align-items. - Phase 5 customtkinter (a callback) — entries, checkboxes, radios. Same shapes; different language.
Part A: The form tags
<form> — the container
Every form is wrapped in a <form> tag:
<form>
<!-- inputs go here -->
<button type="submit">Send</button>
</form>
A <form> does something when submitted. Without
extra setup, it tries to send the data to a URL —
which we don’t have a server for, so today it’ll
look right but not actually send anywhere. We’ll
wire it up with JavaScript starting Session 9.
<input> — text fields and more
The single most useful form tag. Self-closing.
<input type="text" name="username" placeholder="Your name">
The type attribute changes what kind of input:
text— single-line text.email— text, but the browser will validate it looks like an email.password— text, hidden as dots.number— only digits. Can havemin,max,step.date— date picker.color— color picker.file— file upload.checkbox— yes/no toggle.radio— pick one of many.range— slider.submit— a button that submits the form.
The name attribute is the form’s key for that
input. The placeholder is grey text that shows
inside an empty field.
<label> — connect text to inputs
A label tells the user what an input is for. Always use one. Two ways:
<!-- Wrap the input -->
<label>
Email:
<input type="email" name="email">
</label>
<!-- Or use the for attribute -->
<label for="email">Email:</label>
<input type="email" id="email" name="email">
The second form (with for= matching id=) is more
flexible. Both forms link the label to the input —
clicking the label focuses the input. Required for
accessibility (screen readers announce the label).
<textarea> — multi-line text
For long text (comments, messages, descriptions):
<textarea name="message" rows="4" cols="40" placeholder="Your message"></textarea>
Note: <textarea> has separate opening and closing
tags. Whatever’s inside is the initial value.
rows and cols are size hints. CSS overrides them.
<select> — dropdown
<label for="color">Favorite color:</label>
<select id="color" name="color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
Like Phase 5’s CTkOptionMenu. The value is what
gets sent (could differ from what’s shown).
<button> — buttons
<button type="submit">Send</button>
<button type="button">Reset</button>
<button type="reset">Clear all fields</button>
type="submit" submits the form. type="button"
does nothing on its own (you’ll wire it up with JS).
type="reset" clears all the inputs.
<button> is more flexible than
<input type="submit"> — it can contain HTML inside
(icons, multi-color text, etc.).
Build a contact form
Create a new HTML file (or section in your homepage):
<form class="contact-form">
<h2>Get in touch</h2>
<label for="name">Name:</label>
<input type="text" id="name" name="name" placeholder="Your name" required>
<label for="email">Email:</label>
<input type="email" id="email" name="email" placeholder="you@example.com" required>
<label for="topic">Topic:</label>
<select id="topic" name="topic">
<option value="general">General question</option>
<option value="bug">Bug report</option>
<option value="other">Other</option>
</select>
<label for="message">Message:</label>
<textarea id="message" name="message" rows="5" placeholder="Your message" required></textarea>
<label>
<input type="checkbox" name="newsletter">
Subscribe to my newsletter
</label>
<button type="submit">Send</button>
</form>
Save. Reload. A form appears — but it looks raw. CSS next.
What’s new:
required— the browser won’t let the form submit if the field is empty. Free validation.- The checkbox label wraps the checkbox so the whole text is clickable.
<select>with<option>s for the dropdown.
Checkpoint: You have a form with at least one text input, one textarea, one select, and a submit button. This is the natural stop point if class is cut short.
Part B: Style your form
A raw form looks ugly. Time to fix that.
Form container
.contact-form {
max-width: 500px;
margin: 40px auto;
padding: 32px;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
gap: 16px;
}
The form is a flex column with gap: 16px; between
each child. Means we don’t need vertical margins on
each input — clean.
Labels and inputs
.contact-form label {
font-weight: 600;
color: #2c3e50;
}
.contact-form input,
.contact-form textarea,
.contact-form select {
padding: 10px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
font-family: inherit makes form inputs use the
same font as the page. By default, browsers use a
system default that doesn’t match.
Focus state
When the user clicks an input, the border should change to indicate focus:
.contact-form input:focus,
.contact-form textarea:focus,
.contact-form select:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
:focus is a pseudo-class (like :hover). The
outline: none; removes the browser’s default focus
ring; the box-shadow gives a softer one.
Button
.contact-form button {
padding: 12px 24px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
align-self: flex-start; /* don't stretch full width */
}
.contact-form button:hover {
background-color: #2980b9;
}
align-self: flex-start is a Flexbox trick — overrides
the default stretch for just this one item.
The button doesn’t stretch to full width.
Save. Reload. A polished form — clean inputs, focus indicators, styled button.
Checkbox row
The checkbox label wrapping looks weird with the rest of the column. Style it as a horizontal row:
.contact-form label.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: normal;
}
Update the HTML:
<label class="checkbox-label">
<input type="checkbox" name="newsletter">
Subscribe to my newsletter
</label>
Checkbox and text now sit side by side.
Stretch — radio buttons
For “pick one of these”:
<fieldset>
<legend>How did you find me?</legend>
<label><input type="radio" name="source" value="google"> Google</label>
<label><input type="radio" name="source" value="friend"> A friend told me</label>
<label><input type="radio" name="source" value="other"> Other</label>
</fieldset>
<fieldset> groups related form fields with a
border; <legend> is the title for the group.
fieldset {
border: 1px solid #ddd;
border-radius: 4px;
padding: 16px;
}
legend {
padding: 0 8px;
font-weight: 600;
color: #2c3e50;
}
Stretch — input validation hints
The browser auto-validates required fields and
type="email" formats. You can also use:
minlength="3"on text inputs.maxlength="100"on text inputs.min="0" max="100"on number inputs.pattern="[A-Za-z]+"for regex matching.
When the user tries to submit with invalid data, the browser shows a popup. No JavaScript needed.
Stretch — placeholder vs label
placeholder="Your name" shows grey text inside
the empty input. Doesn’t replace the label —
it disappears as soon as you type. Use both:
- Label: what the field is.
- Placeholder: example or format hint.
Extension — disabled and readonly
<input type="text" name="username" value="alex" readonly>
<button type="submit" disabled>Send</button>
readonly — user can’t change the value but can
focus / copy.
disabled — user can’t interact at all (greyed out).
Useful for “you can’t submit until X is true” or “you can’t change your username here.”
Wrap-up
Before we leave, share with the room:
- For everyone — show your form. Did the styling click?
- Did you discover the focus state? (Click in a field; watch the border change.)
- Did anyone notice the form almost works — pressing submit reloads the page with the data in the URL? We’ll fix that with JavaScript.
- Anyone use
requiredand try to submit empty? Free validation.
Today you learned:
<form>— the container.<input>— many types (text, email, number, checkbox, radio, etc.).<label>— connect text to inputs (usefor=/id=, or wrap).<textarea>— multi-line text.<select>+<option>— dropdowns.<button>— buttons.required,placeholder,disabled,readonly— useful attributes.- CSS form styling — focus states, padding, consistent fonts.
:focuspseudo-class for visible feedback.
You can now build any form a website might need — sign-up, login, search, contact, survey, settings. The shapes are the same.
Next week: JavaScript — making the form do something when submitted (and making the page do all sorts of things on demand).
If you missed this session
Open Thonny.
-
Build the basic contact form from Part A.
-
Style it (Part B steps). Make sure focus states work.
-
(Stretch) Add radio buttons in a fieldset.
About 30-45 minutes. By the end you should have a styled form.
Stretch and extension ideas
- Different input types — date, color, range, number, file. Each one has unique behavior.
- Required field validation — the browser handles it.
fieldset+legendfor grouping.- Custom validation messages —
pattern=,minlength=, etc. - Multi-step forms — show some fields, hide others until they’re filled. Advanced (uses JS).
- Search bar — a simple
<form>with one<input type="search">and a button. Common pattern. - Real form on your homepage — add a contact form below your projects.
What’s next
Next week is the big language pivot: JavaScript. We move from HTML/CSS (structure and style) to JS (behavior). Variables, loops, functions — but different syntax from Python. We’ll compare side-by-side.
Session 8: JavaScript — syntax compared to Python
Session 8: JavaScript — syntax compared to Python
Phase 7 — Web · Session 8 of 17
What we’re learning today
You know Python. Today you meet JavaScript — the language the browser runs. The good news: it has the same ideas as Python (variables, conditionals, loops, functions, arrays, objects). The bad news: all the syntax is different. Today is a side-by-side tour of “same idea, different shape.” Next session we’ll use JavaScript to change pages.
You’ll need to remember from last time
- Phase 3 Python — variables, conditionals, loops, functions, lists, dictionaries.
- HTML basics — Sessions 1-2.
<script>tag (we’ll meet it today).- DevTools console — Session 3 stretch.
Part A: JavaScript, side by side with Python
Where JavaScript runs
Two ways to add JavaScript to a page:
1. Inside a <script> tag in HTML:
<body>
<h1>Hi!</h1>
<script>
console.log("Hello from JavaScript!");
</script>
</body>
2. In an external .js file (best):
<script src="app.js"></script>
Same idea as external CSS. Better for organization.
For today’s quick experiments: open your browser’s DevTools console (Right-click → Inspect → Console tab). You can type JavaScript directly there and run it instantly. Like a Python REPL but in the browser.
Try it
Open any page in your browser. Open DevTools (F12 or Ctrl+Shift+I). Click the Console tab. Type:
console.log("Hello!")
Press Enter. “Hello!” appears. That’s how you print in JS.
console.log(...) is the JS equivalent of Python’s
print(...). Anywhere you’d print in Python, you
console.log in JS.
The big differences upfront
# Python # JavaScript
x = 5 let x = 5;
y = "hello" const y = "hello";
print(x) console.log(x);
if x > 0: if (x > 0) {
print("positive") console.log("positive");
}
for i in range(10): for (let i = 0; i < 10; i++) {
print(i) console.log(i);
}
def add(a, b): function add(a, b) {
return a + b return a + b;
}
nums = [1, 2, 3] const nums = [1, 2, 3];
nums.append(4) nums.push(4);
print(len(nums)) console.log(nums.length);
person = {"name": "Alex"} const person = {name: "Alex"};
print(person["name"]) console.log(person.name);
Same shape, different syntax. Notice the patterns:
- Variable declarations with
let(changeable) orconst(won’t change). - Curly braces
{ }instead of indentation. - Parentheses around
if/for/whileconditions. - Semicolons at the end of statements (technically optional, but include them).
- No
def— usefunction.
Variables — let and const
let x = 5;
x = 10; // OK — let is changeable
const y = 5;
y = 10; // Error — const can't change
Use const by default. Only use let when you
know the variable will change.
(Old code uses var — avoid it. Modern JS uses let
and const.)
Strings
Same idea as Python:
const name = "Alex";
const greeting = 'Hi!'; // single quotes also OK
const message = `Hello, ${name}!`; // template literal — like f-strings!
Backticks (`) make a template literal —
the JS version of Python’s f-string. ${expression}
inserts a value. Use these for any string with
variables.
Methods feel familiar:
"hello".length // 5
"hello".toUpperCase() // "HELLO"
"hello".includes("ell") // true
"a,b,c".split(",") // ["a", "b", "c"]
(Note: length is a property, not a function — no
parentheses.)
Numbers
const n = 42;
const pi = 3.14;
n + 1 // 43
n / 2 // 21
n % 3 // 0 — modulo
2 ** 8 // 256 — exponent
Math.floor(3.7) // 3
Math.round(3.7) // 4
Math.random() // random 0 to 1
JS has one number type for both ints and floats.
The Math object holds math functions.
Booleans
const isReady = true;
const isDone = false;
!isReady // false (not)
isReady && isDone // false (and)
isReady || isDone // true (or)
! is “not”, && is “and”, || is “or”. (Python uses
words; JS uses symbols.)
Comparison — === not ==
5 === 5 // true (strict equality)
5 === "5" // false (different types)
5 == "5" // true (loose — converts types)
Always use === (and !==). The == operator
does type coercion which leads to surprising bugs
(0 == "" is true, null == undefined is true).
=== checks both value and type. Way safer.
Conditionals
const score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C or worse");
}
Same shape as Python with curly braces and parens.
Note: else if (two words), not elif.
Loops
// Counting loop
for (let i = 0; i < 10; i++) {
console.log(i);
}
// For-of (like Python's "for x in list")
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
}
// While
let n = 10;
while (n > 0) {
console.log(n);
n = n - 1;
}
The for (let i = 0; i < 10; i++) form is the most
common counting loop. Three parts in the parens:
- Init —
let i = 0(runs once, at start). - Condition —
i < 10(checked each iteration). - After —
i++(runs after each iteration).
i++ is shorthand for i = i + 1. Same as i += 1.
The for (const x of ...) form is for iterating
over arrays — closer to Python’s for x in list.
Functions
function greet(name) {
return "Hello, " + name + "!";
}
console.log(greet("Alex")); // "Hello, Alex!"
The function keyword + name + parens with
parameters + body in braces.
There’s also arrow functions — a shorter syntax:
const greet = (name) => {
return "Hello, " + name + "!";
};
// Or for one-line returns:
const greet = (name) => "Hello, " + name + "!";
const square = (n) => n * n;
Arrow functions are common in modern JS. We’ll see them everywhere.
Arrays — like Python lists
const nums = [1, 2, 3, 4, 5];
nums[0] // 1 — first item
nums[nums.length - 1] // 5 — last item
nums.push(6); // append
nums.pop(); // remove last (returns it)
nums.unshift(0); // prepend
nums.shift(); // remove first
nums.length // count
nums.includes(3) // true
nums.indexOf(3) // 2
// Iterating
for (const n of nums) {
console.log(n);
}
Most Python list methods have JS equivalents with slightly different names.
Objects — like Python dicts
const person = {
name: "Alex",
age: 12,
likes: ["pizza", "soccer"]
};
person.name // "Alex" — dot syntax
person["name"] // also "Alex" — bracket syntax
person.age = 13; // change a value
person.email = "a@b.c"; // add a new key
Object.keys(person) // ["name", "age", "likes", "email"]
JS calls them objects (Python calls them dicts). Same idea: key-value pairs.
The dot syntax (person.name) is the JS norm. Use
brackets when the key is a variable or has special
characters.
Try it all
Try this in your browser’s DevTools console:
const person = { name: "Alex", age: 12 };
function describe(p) {
return `${p.name} is ${p.age} years old`;
}
console.log(describe(person)); // "Alex is 12 years old"
const friends = ["Sam", "Pat", "Jordan"];
for (const friend of friends) {
console.log(`Hi ${friend}!`);
}
const square = (n) => n * n;
console.log(square(7)); // 49
Type each block. Run. Verify the output.
Checkpoint: You’ve run JavaScript in the console — at least one variable, one function, one loop. This is the natural stop point if class is cut short.
Part B: JS in an HTML file
A page that uses JS
Open Thonny. Save a new file as script-test.html:
<!DOCTYPE html>
<html>
<head>
<title>JS test</title>
</head>
<body>
<h1>JavaScript test</h1>
<p>Open the console to see output.</p>
<script>
console.log("Page loaded!");
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(`Color: ${color}`);
}
function add(a, b) {
return a + b;
}
console.log("2 + 3 =", add(2, 3));
</script>
</body>
</html>
Save. Open in browser. Open DevTools → Console. You should see:
Page loaded!
Color: red
Color: green
Color: blue
2 + 3 = 5
The JS ran when the page loaded. The output went to the console.
External JS file
In the same folder, create app.js:
console.log("From external file!");
const numbers = [1, 2, 3, 4, 5];
let total = 0;
for (const n of numbers) {
total = total + n;
}
console.log("Total:", total);
In your HTML, replace the <script>...</script>
block with:
<script src="app.js"></script>
Save both. Reload. The output now comes from
app.js. Same as external CSS — cleaner.
Practice — write a few small functions
In app.js (or directly in the console), try
writing:
// 1. A function that doubles a number
const double = (n) => n * 2;
console.log(double(7)); // 14
// 2. A function that joins names
function joinNames(names) {
return names.join(" and ");
}
console.log(joinNames(["Alex", "Sam", "Pat"])); // "Alex and Sam and Pat"
// 3. A function that filters even numbers
const evens = (numbers) => {
const result = [];
for (const n of numbers) {
if (n % 2 === 0) {
result.push(n);
}
}
return result;
};
console.log(evens([1, 2, 3, 4, 5, 6])); // [2, 4, 6]
// 4. An object describing yourself
const me = {
name: "Alex",
age: 12,
hobbies: ["coding", "soccer"]
};
console.log(`I'm ${me.name}, age ${me.age}.`);
console.log(`My hobbies: ${me.hobbies.join(", ")}`);
Each one mirrors something you’ve done in Python. Try writing more — a max function, a function that counts vowels, anything from Phase 3.
Stretch — array methods
JS arrays have powerful methods. The big ones:
const nums = [1, 2, 3, 4, 5];
nums.map(n => n * 2) // [2, 4, 6, 8, 10]
nums.filter(n => n > 2) // [3, 4, 5]
nums.reduce((sum, n) => sum + n, 0) // 15
nums.forEach(n => console.log(n)) // logs each
These come up constantly in real JS. They’re like
Python’s map, filter, sum — but more idiomatic
in JS.
Stretch — try/catch (like Python’s try/except)
try {
const data = JSON.parse("not json");
} catch (error) {
console.log("Couldn't parse:", error.message);
}
Same idea as Python’s try/except, different syntax.
Wrap-up
Before we leave, share with the room:
- For everyone — show one JavaScript thing you wrote. A function? An object? Something funky?
- Did the syntax differences (braces, parens, semicolons) feel weird at first?
- Which is harder to type — Python or JavaScript?
- Anyone discover an array method that felt magical?
Today you learned:
- JavaScript runs in the browser. Open DevTools console to try things.
console.log(...)=print(...)from Python.- Variables with
let(changeable) andconst(constant). Default toconst. - Strings with single, double, or backticks.
Backticks support
${...}interpolation. ===not==for comparison.- Conditionals with parens around the condition, braces around the body.
- Loops —
for (let i = 0; i < N; i++)andfor (const x of arr). - Functions —
function name() { ... }or arrow(a, b) => a + b. - Arrays —
push,pop,length, etc. - Objects —
{key: value}with dot or bracket access. - External
.jsfiles with<script src="...">.
The thinking is the same as Python. Just the shapes are different.
Next week: the DOM — using JavaScript to reach into the page and change what’s there. Click a button, change the page. The web’s secret power.
If you missed this session
Open your browser. Open DevTools (F12). Click the Console tab.
-
Try the syntax examples in Part A. Run each in the console. Notice the output.
-
Build the test HTML page from Part B with the
<script>block. -
Move the JS to an external
app.jsfile. -
Write a few small functions. Run them in the console.
About 45-60 minutes. By the end you should be comfortable writing simple JS.
Stretch and extension ideas
- Array methods —
map,filter,reduce,forEach. Practice converting Python loops to these. try/catchfor error handling.JSON.parseandJSON.stringify— convert between objects and JSON strings.setTimeout(fn, 1000)— run a function after 1 second. (Async, sort of.)- Read MDN’s JavaScript Guide — the canonical reference.
What’s next
Next week: the DOM — Document Object Model.
JavaScript’s interface to the page. We’ll use
document.querySelector(...) to find elements,
addEventListener(...) to react to clicks, and
element.textContent = ... to change what’s on the
page. This is where JS gets exciting.
Session 9: The DOM — querySelector and events
Session 9: The DOM — querySelector and events
Phase 7 — Web · Session 9 of 17
What we’re learning today
You know JavaScript syntax. Today you learn how JS reaches into the page and changes things. Click a button → text updates. Type in a field → preview changes. Toggle a class → theme switches. This is the DOM (Document Object Model) — the bridge between your JS and the HTML on the page. By the end of class you’ll have built an interactive counter and a live-updating greeting.
You’ll need to remember from last time
- JavaScript syntax — variables, functions, conditionals (Session 8).
console.logfor debugging.- HTML structure — IDs and classes (Sessions 2-3).
<input>and<button>(Session 7).
Part A: Reaching into the page
What’s the DOM?
When the browser loads your HTML, it builds a tree of objects — one for every tag, one for every text node. This tree is called the DOM (Document Object Model).
JavaScript can:
- Find elements in the tree.
- Read their content (
innerText,value). - Change their content.
- Listen for events (clicks, key presses).
- Add or remove classes (which triggers CSS).
- Create or destroy elements.
The starting object is document — JS’s handle to
the page.
Find an element
The most common way:
const heading = document.querySelector("h1");
document.querySelector(...) takes a CSS selector
(any selector you’d use in CSS) and returns the
first matching element. So:
document.querySelector("h1") // first <h1>
document.querySelector(".card") // first .card
document.querySelector("#main-title") // the #main-title
document.querySelector(".card h2") // first h2 inside a card
Same selectors as CSS. One API for both.
For all matches, use querySelectorAll:
const cards = document.querySelectorAll(".card");
Returns a list-like thing (a NodeList) of all
matching elements.
Try it
Open Thonny. Save a new file as dom-test.html:
<!DOCTYPE html>
<html>
<head>
<title>DOM test</title>
</head>
<body>
<h1 id="title">Hello!</h1>
<p>This is a paragraph.</p>
<button id="changeBtn">Change the title</button>
<script>
const heading = document.querySelector("#title");
console.log(heading);
console.log(heading.textContent);
</script>
</body>
</html>
Save. Open in browser. Open DevTools console.
You see two outputs:
<h1 id="title">Hello!</h1>— the actual HTML element.Hello!— its text content.
textContent is the text inside an element. You can
also write to it:
heading.textContent = "I changed!";
Type that in the console. The page updates
immediately. The <h1> now says “I changed!”
You just changed the page from JavaScript. That’s the power.
Listen for events
Right now the button does nothing. Add a click handler:
const button = document.querySelector("#changeBtn");
button.addEventListener("click", () => {
heading.textContent = "Button was clicked!";
});
Save. Reload. Click the button. The heading changes.
What’s happening:
addEventListener("click", fn)attaches a function to the button.- When the button is clicked, the function runs.
- The function changes
heading.textContent.
The function is an arrow function — Session 8. The
() => { ... } is shorthand for “a function that
takes no arguments and runs this code.”
Common events
element.addEventListener("click", handler); // mouse click
element.addEventListener("dblclick", handler); // double-click
element.addEventListener("input", handler); // user typed in input
element.addEventListener("submit", handler); // form submit
element.addEventListener("change", handler); // input value changed
element.addEventListener("mouseenter", handler); // mouse entered
element.addEventListener("mouseleave", handler); // mouse left
element.addEventListener("keydown", handler); // key pressed
Most user interactions have an event you can listen
for. click and input are the two you’ll
use most.
Counter — a complete tiny app
Open Thonny. Save a new file as counter.html:
<!DOCTYPE html>
<html>
<head>
<title>Counter</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 80px auto;
text-align: center;
}
#count {
font-size: 80px;
margin: 20px 0;
}
button {
font-size: 24px;
padding: 10px 20px;
margin: 0 5px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Counter</h1>
<div id="count">0</div>
<button id="minus">-1</button>
<button id="reset">Reset</button>
<button id="plus">+1</button>
<script>
let count = 0;
const display = document.querySelector("#count");
const plusBtn = document.querySelector("#plus");
const minusBtn = document.querySelector("#minus");
const resetBtn = document.querySelector("#reset");
plusBtn.addEventListener("click", () => {
count = count + 1;
display.textContent = count;
});
minusBtn.addEventListener("click", () => {
count = count - 1;
display.textContent = count;
});
resetBtn.addEventListener("click", () => {
count = 0;
display.textContent = count;
});
</script>
</body>
</html>
Save. Open in browser. Click the buttons. The counter goes up and down. Reset returns to 0.
This is your first interactive web app.
The pattern:
- State — a variable (
count). - DOM references — find elements (
display, buttons). - Event listeners — wire up buttons.
- In each handler: update state, sync DOM.
This is the fundamental shape of every interactive JS app. Phase 5 customtkinter had the same shape with different syntax. The DOM is the JS version of “reach into the UI and change it.”
Checkpoint: You have a working counter with three buttons that change a displayed number. This is the natural stop point if class is cut short.
Part B: Reading inputs and changing classes
Reading from an input
For text inputs, the value lives in .value (not
.textContent):
<input type="text" id="nameInput" placeholder="Your name">
<button id="greetBtn">Greet me</button>
<p id="greeting"></p>
<script>
const input = document.querySelector("#nameInput");
const button = document.querySelector("#greetBtn");
const greeting = document.querySelector("#greeting");
button.addEventListener("click", () => {
const name = input.value;
greeting.textContent = `Hello, ${name}!`;
});
</script>
Type a name. Click the button. Your name shows up.
Note: .textContent for paragraphs and headings;
.value for input fields and textareas.
Live updates with the input event
input.addEventListener("input", () => {
greeting.textContent = `Hello, ${input.value}!`;
});
Now the greeting updates as you type — no button needed. Try it.
The input event fires on every keystroke. Modern
reactive apps work this way.
Toggling a class
The most common DOM trick — add or remove a CSS class to change appearance:
<div id="box" class="box">A box</div>
<button id="toggleBtn">Toggle dark mode</button>
<style>
.box {
padding: 30px;
background-color: white;
color: black;
transition: all 0.3s;
}
.box.dark {
background-color: black;
color: white;
}
</style>
<script>
const box = document.querySelector("#box");
const toggleBtn = document.querySelector("#toggleBtn");
toggleBtn.addEventListener("click", () => {
box.classList.toggle("dark");
});
</script>
Click the button. Box turns dark. Click again. Light.
classList has these methods:
add("classname")— add the class.remove("classname")— remove it.toggle("classname")— flip (add if absent, remove if present).contains("classname")— true/false.
Strong recommendation: Toggle CSS classes from JS, don’t change
.style.Xdirectly. Why? CSS keeps the visual decisions; JS keeps the what changed decisions. Cleaner separation.
Build it — a live-updating greeting
Open Thonny. Save as greeting.html:
<!DOCTYPE html>
<html>
<head>
<title>Greeting</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 80px auto;
text-align: center;
transition: background-color 0.3s, color 0.3s;
}
body.dark {
background-color: #1a1a2e;
color: white;
}
input, button {
font-size: 18px;
padding: 10px;
margin: 5px;
}
#greeting {
font-size: 36px;
margin: 30px 0;
}
</style>
</head>
<body>
<h1>Greeting</h1>
<input type="text" id="nameInput" placeholder="Your name">
<button id="darkBtn">Toggle dark mode</button>
<p id="greeting">Hello, ?</p>
<script>
const input = document.querySelector("#nameInput");
const greeting = document.querySelector("#greeting");
const darkBtn = document.querySelector("#darkBtn");
input.addEventListener("input", () => {
const name = input.value || "?";
greeting.textContent = `Hello, ${name}!`;
});
darkBtn.addEventListener("click", () => {
document.body.classList.toggle("dark");
});
</script>
</body>
</html>
Save. Reload. Type a name — greeting updates live. Click “Toggle dark mode” — page goes dark. Click again — back to light.
The input.value || "?" trick: if input is empty,
use “?” instead. Real little touch.
Stretch — multiple list items
Show a list with class toggles:
<ul id="todo-list">
<li>Pizza</li>
<li>Soccer</li>
<li>Reading</li>
</ul>
<style>
li { cursor: pointer; transition: color 0.2s; }
li.done { color: gray; text-decoration: line-through; }
</style>
<script>
const items = document.querySelectorAll("#todo-list li");
items.forEach(item => {
item.addEventListener("click", () => {
item.classList.toggle("done");
});
});
</script>
querySelectorAll returns multiple. .forEach runs a
function on each. Click any item — it gets struck
through. Click again — restored.
This is the seed of the todo list we build properly next session.
Stretch — preventDefault on form submit
const form = document.querySelector("form");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the default form submit
// now do whatever you want with the form data
const name = document.querySelector("#name").value;
console.log("Submitted with name:", name);
});
event.preventDefault() stops the form from doing its
default behavior (submitting to a URL). Now your JS
handles it. Crucial for forms in JS.
Extension — element creation
You can build elements from JS:
const newItem = document.createElement("li");
newItem.textContent = "New thing!";
document.querySelector("#todo-list").appendChild(newItem);
Three lines:
createElement("li")— make a new<li>element (not yet in the page).- Set its content.
appendChildadds it to the parent.
This is how todo apps add new items dynamically. We’ll use this constantly next session.
Wrap-up
Before we leave, share with the room:
- For everyone — show your counter or greeting.
- Did the live-updating-as-you-type feel different? More “modern”?
- For the kids who tried the toggleable list — how satisfying was the strikethrough-on-click?
- Is the DOM what you expected?
Today you learned:
- The DOM — JavaScript’s interface to the page.
document.querySelector(...)— find one element.document.querySelectorAll(...)— find all matches..textContent— read or change text..value— read or change input value..classList.add/remove/toggle— change CSS classes (preferred over.style.X).addEventListener("event", handler)— react to user actions.- Common events: click, input, submit, change.
event.preventDefault()— stop default behaviors.document.createElement(...)+appendChildfor new elements.
The pattern: state + DOM references + event listeners + sync. Same as customtkinter, different language.
Next week: a full interactive todo list — combining everything. Add items, delete, mark complete. Real app.
If you missed this session
Open Thonny.
-
Build the basic
dom-test.htmlfrom Part A. Get the title to change when you click the button. -
Build the counter from Part A. Plus, minus, reset.
-
Build the greeting from Part B with live updates and dark mode.
-
(Stretch) Build the toggleable list.
About 45-60 minutes. By the end you should have at least two interactive pages.
Stretch and extension ideas
- Inline editing — click text to edit it.
- Multiple themes — buttons for dark, sepia, blue, etc.
- Keyboard shortcuts — listen for keys. Press Space to count up; press R to reset.
- A small calculator — two number inputs, four operation buttons, a result display.
- A color picker that updates the page’s
background:
(One time whencolorInput.addEventListener("input", () => { document.body.style.backgroundColor = colorInput.value; });.style.Xis fine — the color is truly dynamic.) - A character counter for textarea — show “500/1000 characters” as the user types.
What’s next
Next week: build a real todo list — add items, mark them done, delete them. Combine everything from Sessions 1-9. Your first JS-powered app.
Session 10: Build an interactive todo list
Session 10: Build an interactive todo list
Phase 7 — Web · Session 10 of 17
What we’re learning today
You built a todo list in Phase 5 customtkinter. Today you build the same app, in the browser — HTML for structure, CSS for style, JS for behavior. By the end, you’ll have a working todo app you can open in any browser. Next session we make it remember todos between page reloads.
This is the integration project for the JavaScript movement.
You’ll need to remember from last time
document.querySelectorandaddEventListener(Session 9)..textContent,.value,.classList(Session 9).document.createElement+appendChild(Session 9 stretch).event.preventDefault()for form submissions.- JavaScript syntax (Session 8).
- Phase 5 todo app — the customtkinter version is the same app.
Part A: Build the todo list
Open Thonny. Create a folder for this project. Create
two files: index.html and app.js.
We’ll build in steps. Run after every step.
Step 1 — HTML structure
index.html:
<!DOCTYPE html>
<html>
<head>
<title>My Todos</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main>
<h1>My Todos</h1>
<form id="todoForm">
<input type="text" id="todoInput" placeholder="What needs doing?" required>
<button type="submit">Add</button>
</form>
<ul id="todoList"></ul>
</main>
<script src="app.js"></script>
</body>
</html>
Save. Open in browser. Plain page with a heading, a form, and an empty list.
Step 2 — Add the first todo
In app.js:
const form = document.querySelector("#todoForm");
const input = document.querySelector("#todoInput");
const list = document.querySelector("#todoList");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the page reloading
const text = input.value.trim();
if (text === "") return;
const item = document.createElement("li");
item.textContent = text;
list.appendChild(item);
input.value = ""; // clear the input
});
Save. Reload. Type a todo. Click Add. The todo appears in the list. Add another. Add several. It works.
What’s happening:
event.preventDefault()— stop the form from reloading the page (its default behavior).input.value.trim()— get the typed text, remove leading/trailing whitespace.if (text === "") return;— ignore empty submissions.document.createElement("li")— make a new<li>element.item.textContent = text;— set its text.list.appendChild(item);— add it to the<ul>.input.value = "";— clear the input for the next entry.
Step 3 — Style it
Create styles.css:
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, sans-serif;
background-color: #f0f0f0;
color: #333;
margin: 0;
padding: 40px 20px;
}
main {
max-width: 500px;
margin: 0 auto;
background-color: white;
padding: 32px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
h1 {
margin-top: 0;
color: #2c3e50;
}
#todoForm {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
#todoInput {
flex: 1;
padding: 10px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
}
#todoInput:focus {
outline: none;
border-color: #3498db;
}
#todoForm button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
#todoForm button:hover {
background-color: #2980b9;
}
#todoList {
list-style: none;
padding: 0;
margin: 0;
}
#todoList li {
padding: 12px;
background-color: #fafafa;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
Save. Reload. The todo app is now styled — a white card with the form on top, todos below as neat cards.
list-style: none removes the default bullet points
on <ul>s.
Step 4 — Delete buttons
When you create each <li>, also create an “×”
button inside it:
In app.js, replace the addEventListener("submit", ...) handler with this:
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (text === "") return;
const item = document.createElement("li");
const span = document.createElement("span");
span.textContent = text;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "×";
deleteBtn.className = "delete-btn";
deleteBtn.addEventListener("click", () => {
item.remove();
});
item.appendChild(span);
item.appendChild(deleteBtn);
list.appendChild(item);
input.value = "";
});
Add to styles.css:
.delete-btn {
background: none;
border: none;
color: #999;
font-size: 24px;
cursor: pointer;
line-height: 1;
padding: 0 4px;
}
.delete-btn:hover {
color: #e74c3c;
}
Save. Reload. Each todo now has an × button. Click × to delete that todo. Add more, delete others. Works.
What’s new:
- A wrapper
<li>with two children: a<span>for the text and a<button>for the delete. item.remove()— built-in DOM method to remove an element from the page.deleteBtn.className = "delete-btn"— set the CSS class.
Step 5 — Mark complete
Click the text to toggle “done” state:
In app.js, after creating the span:
span.addEventListener("click", () => {
item.classList.toggle("done");
});
span.style.cursor = "pointer"; // hint that it's clickable
In styles.css, add:
#todoList li.done span {
color: #999;
text-decoration: line-through;
}
Save. Reload. Click any todo’s text. It gets struck through. Click again. Restored.
Now you can add, complete, and delete. Real todo app shape.
Checkpoint: You have a working todo app — add, complete, delete. This is the natural stop point if class is cut short.
Part B: Polish
Time to make it nicer.
Empty state
When there are no todos, show a friendly message
instead of a blank list. Add to your HTML, just
inside <main> after the form:
<p id="emptyMessage">Nothing to do! Add a todo above.</p>
Add CSS:
#emptyMessage {
color: #aaa;
text-align: center;
margin: 20px 0;
}
Manage the message from JS. After every change to the list:
function updateEmptyMessage() {
const message = document.querySelector("#emptyMessage");
if (list.children.length === 0) {
message.style.display = "block";
} else {
message.style.display = "none";
}
}
Call updateEmptyMessage() after adding and after
deleting. Or wrap the change-and-update in a
single function so you always sync.
Counter
How many todos do you have? How many are done? Add a header above the list:
<p id="counter"></p>
function updateCounter() {
const total = list.children.length;
const done = document.querySelectorAll("#todoList li.done").length;
document.querySelector("#counter").textContent = `${done}/${total} done`;
}
Call updateCounter() whenever you add, delete, or
toggle.
#counter {
text-align: right;
color: #666;
margin: 0 0 10px 0;
}
Save. Reload. The counter updates live.
Stretch — clear all
A button to wipe the list:
<button id="clearBtn">Clear all</button>
const clearBtn = document.querySelector("#clearBtn");
clearBtn.addEventListener("click", () => {
if (confirm("Are you sure?")) {
list.innerHTML = "";
updateCounter();
updateEmptyMessage();
}
});
confirm("...") shows a built-in OK/Cancel dialog.
Returns true if user clicked OK.
list.innerHTML = "" removes all children at once.
Stretch — Enter to add
Already works! Forms submit on Enter by default. Type a todo, press Enter — added. Try it.
Stretch — show added time
Add a timestamp:
const timeSpan = document.createElement("small");
const now = new Date();
timeSpan.textContent = now.toLocaleTimeString();
timeSpan.className = "time";
item.appendChild(timeSpan);
.time {
color: #999;
font-size: 12px;
margin-left: 8px;
}
Each new todo shows when it was added.
Extension — refactor with a class
If your app.js is getting long, wrap it in a
TodoApp class (like Phase 5 Session 6’s class
refactor):
class TodoApp {
constructor() {
this.list = document.querySelector("#todoList");
this.form = document.querySelector("#todoForm");
this.input = document.querySelector("#todoInput");
this.form.addEventListener("submit", (e) => this.add(e));
}
add(event) {
event.preventDefault();
const text = this.input.value.trim();
if (text === "") return;
this.createItem(text);
this.input.value = "";
}
createItem(text) {
const item = document.createElement("li");
// ... build item, attach listeners ...
this.list.appendChild(item);
}
}
new TodoApp();
Same code, organized as a class. Real production shape.
Extension — push to GitHub
Treat this todo as a real project. Commit it. Push to a GitHub repo. (You did this in Phase 6 Session 7 with Pong — same flow.)
You’re building a portfolio.
Wrap-up
Before we leave, share with the room:
- For everyone — show your todo app. How many todos did you add to test?
- For the kids who added the counter — does the live update feel modern?
- For the kids who tried
confirm()— fun built-in dialog, right? - Anyone notice you have to re-add todos every time you reload? Next week we fix that.
Today you built a real interactive web app. Not a toy. The kind of thing you could actually use to track real tasks (in this single browser tab, until next session adds storage).
You used:
- Form submit with
preventDefault. - Reading input with
.value. - Creating elements with
createElementandappendChild. - Deleting elements with
.remove(). - Toggling state with
classList.toggle. - Built-in dialogs like
confirm(). - The pattern — state in DOM, JS reads/changes, events drive updates.
This is the production shape of every interactive web app. Same patterns power the apps you use every day — Trello, Slack, Twitter (each is a more elaborate version).
Next week: localStorage — make your todos persist between page reloads. Real persistence in the browser.
If you missed this session
Open Thonny.
-
Create a folder. Inside,
index.html,app.js,styles.css. -
Build the HTML shell from Step 1.
-
Build the add-todo logic from Step 2.
-
Style with the CSS from Step 3.
-
Add delete buttons (Step 4) and complete-toggle (Step 5).
About 60-90 minutes — this is a substantial session.
Stretch and extension ideas
- Counter — show “X/Y done.”
- Empty state — friendly message when list is empty.
- Clear all with
confirm(). - Timestamp on each todo.
- Refactor as a class.
- Edit a todo — double-click to make it editable. Replace span with input on dblclick; save on Enter or blur.
- Filter — show all / active / completed.
- Drag to reorder — advanced (uses
dragstart,dragover,dropevents). - Push to GitHub.
What’s next
Next week: localStorage — your todos survive between page reloads. Close the tab, come back later, todos are still there. The “wow” moment of the JavaScript movement.
Session 11: localStorage — make state survive
Session 11: localStorage — make state survive
Phase 7 — Web · Session 11 of 17
What we’re learning today
Last session your todo app worked great — until you reloaded the page. Then everything vanished. Today we fix that with localStorage — a tiny key-value store the browser provides for every website. Save your todos to localStorage; load them on page open. Close the browser, come back tomorrow, todos are still there. Real persistence.
You’ll need to remember from last time
- Your todo app from Session 10.
JSON.stringifyandJSON.parse(Session 8 stretch — we’ll cover them properly today).- DOM manipulation —
createElement,appendChild,remove. - Phase 5 Session 6 stretch — JSON file persistence in customtkinter. Same idea, different storage.
Part A: localStorage basics
What’s localStorage?
localStorage is a key-value store built into
every browser. It stores strings (and only
strings) under string keys, and the data persists
forever (until explicitly cleared, or until the
user clears browser data).
It’s:
- Per-site. Each website has its own localStorage; sites can’t read each other’s.
- Per-browser. Different browsers have separate storage. Chrome’s localStorage is separate from Firefox’s.
- Limited size. ~5-10 MB per site (tons for text; not for big images).
- Synchronous and instant. No callbacks, no await — just call and use.
The API — three methods
localStorage.setItem("key", "value"); // save
const value = localStorage.getItem("key"); // load
localStorage.removeItem("key"); // delete
localStorage.clear(); // delete everything
That’s it. Four methods. Easy.
Try it
Open any HTML file (or the homepage you’ve been building) in your browser. Open the DevTools console (F12 → Console).
Type:
localStorage.setItem("name", "Alex");
Press Enter. Saved.
Now:
localStorage.getItem("name"); // "Alex"
Returns the value. Reload the page. Try
getItem("name") again. Still “Alex”. The data
survived the reload.
Close the browser tab. Reopen the page in a fresh
tab. getItem("name") still returns “Alex”. The
data survives across sessions.
This is local persistent storage in the browser.
See it in DevTools
In DevTools, switch to the Application tab (in Chrome) or Storage tab (in Firefox). Find Local Storage in the left sidebar. Click it.
You see all the keys you’ve stored, with their values. You can edit or delete them directly here. Useful for debugging.
Strings only
localStorage only stores strings. What if you
want to save a list?
const todos = ["pizza", "soccer", "reading"];
localStorage.setItem("todos", todos); // saves "pizza,soccer,reading" — actually a string
localStorage.getItem("todos"); // "pizza,soccer,reading"
It works, but you got a string back, not an array.
JSON to the rescue
The standard way to save complex data is JSON serialization:
// Save (object → string)
const todos = ["pizza", "soccer", "reading"];
localStorage.setItem("todos", JSON.stringify(todos));
// Load (string → object)
const loaded = JSON.parse(localStorage.getItem("todos"));
console.log(loaded); // ["pizza", "soccer", "reading"]
JSON.stringify(value) turns any object/array into
a JSON string. JSON.parse(string) turns it back.
JSON works for any combination of:
- Strings, numbers, booleans, null.
- Arrays.
- Objects (with string keys).
Functions, undefined, and circular references break JSON. Stick to plain data.
Build a settings example
Open Thonny. Save a new file as settings.html:
<!DOCTYPE html>
<html>
<head>
<title>Settings demo</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 60px auto;
padding: 20px;
transition: all 0.3s;
}
body.dark {
background-color: #1a1a2e;
color: white;
}
label {
display: block;
margin: 12px 0;
}
</style>
</head>
<body>
<h1>Settings</h1>
<label>
<input type="checkbox" id="darkToggle">
Dark mode
</label>
<label>
Your name:
<input type="text" id="nameInput">
</label>
<script src="settings.js"></script>
</body>
</html>
settings.js:
const darkToggle = document.querySelector("#darkToggle");
const nameInput = document.querySelector("#nameInput");
// Load saved settings
const isDark = localStorage.getItem("dark") === "true";
const savedName = localStorage.getItem("name") || "";
// Apply them
darkToggle.checked = isDark;
nameInput.value = savedName;
if (isDark) document.body.classList.add("dark");
// Save on change
darkToggle.addEventListener("change", () => {
localStorage.setItem("dark", darkToggle.checked);
document.body.classList.toggle("dark", darkToggle.checked);
});
nameInput.addEventListener("input", () => {
localStorage.setItem("name", nameInput.value);
});
Save. Reload. Set dark mode. Type your name. Reload the page (or close and reopen the tab). Dark mode is still on. Name is still there. Settings persist.
A few details:
localStorage.getItem("dark") === "true"— localStorage returns strings, so we compare to the string"true".localStorage.getItem("name") || ""— if no saved name (returnsnull), default to empty string.darkToggle.checked— checkbox state (true/ false).classList.toggle("dark", darkToggle.checked)— second arg forces add if true, remove if false.
Checkpoint: You have a page with at least one setting that persists across reloads. This is the natural stop point if class is cut short.
Part B: Persist the todo list
Open your todo app from Session 10. Add persistence.
The pattern
After every change to the list, save. On page load, restore.
Add to the top of app.js:
function saveTodos() {
const items = list.querySelectorAll("li");
const todos = [];
items.forEach(item => {
const text = item.querySelector("span").textContent;
const done = item.classList.contains("done");
todos.push({ text, done });
});
localStorage.setItem("todos", JSON.stringify(todos));
}
function loadTodos() {
const data = localStorage.getItem("todos");
if (!data) return;
const todos = JSON.parse(data);
todos.forEach(({ text, done }) => {
addItem(text, done);
});
}
Refactor your add-item code into a function so both the form submit and load can use it:
function addItem(text, done = false) {
const item = document.createElement("li");
if (done) item.classList.add("done");
const span = document.createElement("span");
span.textContent = text;
span.style.cursor = "pointer";
span.addEventListener("click", () => {
item.classList.toggle("done");
saveTodos();
});
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "×";
deleteBtn.className = "delete-btn";
deleteBtn.addEventListener("click", () => {
item.remove();
saveTodos();
});
item.appendChild(span);
item.appendChild(deleteBtn);
list.appendChild(item);
}
Update the form handler:
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (text === "") return;
addItem(text);
saveTodos();
input.value = "";
});
Call loadTodos() once on page load:
loadTodos();
Save. Reload. Add some todos. Toggle done on a few. Close the browser tab. Reopen the page. Your todos are still there, in their done/not-done states.
This is real persistence. The app remembers between sessions. You could close your laptop, come back tomorrow, and see your todos.
Stretch — a “clear all” with persistence
clearBtn.addEventListener("click", () => {
if (confirm("Clear all todos?")) {
list.innerHTML = "";
saveTodos(); // saves an empty list
}
});
saveTodos() after the clear writes the (now
empty) list to localStorage. Otherwise reload would
restore them.
Stretch — show storage usage
const data = localStorage.getItem("todos") || "";
console.log(`Storage: ${data.length} bytes`);
Tiny inspection of how much you’re storing.
Extension — settings + todos in one app
Combine the settings (dark mode toggle) with the
todo persistence. Two separate keys in localStorage
("darkMode" and "todos").
Extension — multiple lists
Save an object that holds multiple lists:
const allLists = {
"Today": ["pizza", "soccer"],
"Tomorrow": ["doctor", "homework"],
"Sometime": ["learn french", "build robot"]
};
localStorage.setItem("lists", JSON.stringify(allLists));
Lets your app handle multiple categories. UI gets more complex (need to switch between lists), but the storage pattern is the same.
Extension — export/import
Add buttons to export the data (download as a JSON file) and import (upload a JSON file). Useful for moving data between machines.
function exportTodos() {
const data = localStorage.getItem("todos") || "[]";
const blob = new Blob([data], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "todos.json";
a.click();
}
Advanced. Real-app feature.
Wrap-up
Before we leave, share with the room:
- For everyone — show your persistent todo. Add some, reload, see them survive.
- For the kids who tried clearing — does it now stay clear after reload?
- Did seeing your data in DevTools’ Application panel feel cool? Like seeing inside?
- Anyone notice that this only works on your browser? Different browser = different storage.
Today you learned:
localStorage— browser-built-in key-value store.- Four methods:
setItem,getItem,removeItem,clear. - Strings only — use
JSON.stringify/JSON.parsefor objects and arrays. getItemreturnsnullif no key — use||fallback.- DevTools Application/Storage tab to inspect.
- Pattern: save on every change, load on page start.
This is the simplest browser persistence — no backend, no database, just your code and the browser. Real apps use this for:
- Settings (dark mode, language preference).
- Drafts (auto-save what the user is typing).
- “Recent searches” / “recently viewed.”
- Authentication tokens.
- Game saves (the kid’s sidequest).
For data that needs to be shared across devices, you’d need a real backend (server + database). That’s Phase 8 (Flask).
Next week: Canvas — drawing graphics in the browser. The Pygame patterns transfer directly.
If you missed this session
Open Thonny. You need your Session 10 todo app.
-
Open
app.js. Add thesaveTodos()andloadTodos()functions from the handout. -
Refactor add logic into an
addItem(text, done)function that both form-submit and load can use. -
Call
saveTodos()after every change. CallloadTodos()once at startup. -
Add some todos, reload, see them persist.
-
(Stretch) Add the settings example or any extension.
About 45-60 minutes. By the end your todo app should persist across reloads.
Stretch and extension ideas
- Settings + todos in one app.
- Multiple lists with names.
- Export / import as JSON files.
- Storage size display — tiny info bar showing current usage.
- “Last edited” timestamp — save when each todo was last changed.
- Auto-save drafts — save the input field as you type, restore on reload (in case you closed by accident).
- Different storage:
sessionStorage(same API, but only lives for the current tab — clears on close).
What’s next
Next week: Canvas — JavaScript’s drawing surface. Lines, shapes, images, animation. The Pygame mental model directly transfers — same frame loop, same drawing primitives, in the browser. Followed by Session 13’s canvas mini-game.
Session 12: Canvas — drawing in JavaScript
Session 12: Canvas — drawing in JavaScript
Phase 7 — Web · Session 12 of 17
What we’re learning today
You can draw in Pygame. Today you draw in the
browser — using the <canvas> element and JavaScript.
The drawing API looks very similar to Pygame’s: same
shapes, same coordinate system (top-left = 0,0, y goes
down), same logic. The mental model from Phase 6
transfers directly. By the end of class you’ll have
a small scene drawn in the browser. Next session: a
mini-game.
You’ll need to remember from last time
- JavaScript syntax (Session 8).
document.querySelector(Session 9).- Phase 6 Sessions 1-2 — Pygame’s
pygame.draw.rect,circle,line, the coordinate system. All of that mental model applies today.
Part A: The canvas element
What’s a canvas?
The <canvas> element is a rectangular drawing
surface in HTML. It looks like nothing on its own —
just a blank rectangle. You use JavaScript to draw
on it.
<canvas id="myCanvas" width="600" height="400"></canvas>
The width and height set the canvas’s pixel
dimensions. (Always set them in HTML, not CSS — CSS
just scales the canvas, which can blur it.)
Get the drawing context
To draw, you need the 2D drawing context:
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
ctx (short for “context”) is your drawing handle.
Almost every canvas method is called on ctx.
(There’s also getContext("webgl") for 3D — way out
of scope.)
Try it — your first canvas
Open Thonny. Save a new file as canvas-test.html:
<!DOCTYPE html>
<html>
<head>
<title>Canvas test</title>
<style>
canvas {
border: 1px solid #999;
display: block;
margin: 20px auto;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="600" height="400"></canvas>
<script>
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
// Background
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
// A red box
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
</script>
</body>
</html>
Save. Open in browser. A canvas with a sky-blue background and a red square in the corner.
What’s happening:
ctx.fillStyle = "..."sets the color for any shape we draw next.ctx.fillRect(x, y, width, height)draws a filled rectangle.
Compare to Pygame:
# Pygame
screen.fill((135, 206, 235))
pygame.draw.rect(screen, (255, 0, 0), (50, 50, 100, 100))
// Canvas
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
ctx.fillStyle = "red";
ctx.fillRect(50, 50, 100, 100);
Almost identical. Different language, same model.
The coordinate system
Same as Pygame. Same as Phase 7’s HTML positions.
(0, 0)is the top-left corner.xincreases to the right.yincreases downward.
If you remember Phase 6, this is automatic.
Filled vs outlined shapes
// Filled — interior color
ctx.fillStyle = "blue";
ctx.fillRect(200, 50, 80, 80);
// Outlined — border color
ctx.strokeStyle = "blue";
ctx.lineWidth = 4;
ctx.strokeRect(300, 50, 80, 80);
fillRect and strokeRect (filled and outlined,
respectively).
fillStyle and strokeStyle (the two color
properties).
lineWidth (border thickness).
Circles (with arc)
Canvas doesn’t have drawCircle. It has arc,
which draws part of a circle (or all of it, if you
go full 360°).
The pattern for a full circle:
ctx.beginPath();
ctx.arc(300, 200, 50, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fillStyle = "yellow";
ctx.fill();
arc(x, y, radius, startAngle, endAngle) draws an
arc:
(x, y)— the center.radius— size.startAngle— where to start (in radians).endAngle— where to end (in radians).
Math.PI * 2 is a full 360° circle.
The beginPath() / fill() pair is required for
arcs. Frame:
“Arcs are drawn as a path. Tell canvas you’re starting a new path (
beginPath), describe the shape (arc(...)), then eitherfill()orstroke()to actually draw it.”
Lines work the same way (we’ll see in a moment).
A complete scene
Replace your script with:
const canvas = document.querySelector("#myCanvas");
const ctx = canvas.getContext("2d");
// Sky background
ctx.fillStyle = "#87ceeb";
ctx.fillRect(0, 0, 600, 400);
// Ground
ctx.fillStyle = "#3a7d3a";
ctx.fillRect(0, 300, 600, 100);
// Sun
ctx.fillStyle = "#ffeb3b";
ctx.beginPath();
ctx.arc(500, 80, 40, 0, Math.PI * 2);
ctx.fill();
// House body
ctx.fillStyle = "#c69876";
ctx.fillRect(220, 200, 160, 100);
// Roof — using lineTo
ctx.fillStyle = "#8b3030";
ctx.beginPath();
ctx.moveTo(210, 200);
ctx.lineTo(390, 200);
ctx.lineTo(300, 130);
ctx.closePath();
ctx.fill();
// Door
ctx.fillStyle = "#5a3a1a";
ctx.fillRect(280, 250, 40, 50);
Save. Reload. A scene — sky, ground, sun, house, door.
What’s new:
moveTo(x, y)— move the “pen” to that point without drawing.lineTo(x, y)— draw a line from the pen’s position to that point.closePath()— close the path back to where it started.
This is how you draw triangles, polygons, and any
custom shape. Phase 6 had pygame.draw.polygon —
this is the canvas equivalent.
Text
ctx.font = "30px Arial";
ctx.fillStyle = "white";
ctx.fillText("Hello!", 100, 100);
fillText(text, x, y) draws text at a position.
The font property is a CSS-style font string.
For text alignment:
ctx.textAlign = "center"; // or "left" (default), "right"
ctx.fillText("Centered", 300, 200);
Where else does this look like CSS?
A few canvas properties take CSS-like values:
ctx.fillStyle = "red"or"#ff0000"or"rgba(255, 0, 0, 0.5)"ctx.font = "30px Arial"ctx.lineWidth = 4
Familiar territory. The web is unified.
Checkpoint: Your canvas has at least 4 different shapes (rectangle, circle, polygon, text), in different colors. This is the natural stop point if class is cut short.
Part B: Build a scene
Time to make something yours.
Pick a subject
Same kind of exercise as Phase 6 Session 2 (the shape-drawing scene). Pick:
- A face — circles for head and eyes, lines for smile.
- A landscape — mountains as triangles, sky, sun, trees.
- A rocket ship — rectangles + triangles + circle windows.
- A city skyline — rectangles of varying heights.
- A bouncing logo screensaver (like the old DVD player).
- An abstract pattern — circles in a grid, color gradients.
Use loops
JavaScript loops work in canvas too. A row of stars:
for (let i = 0; i < 50; i++) {
const x = Math.random() * 600;
const y = Math.random() * 200;
const r = Math.random() * 3 + 1;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = "white";
ctx.fill();
}
50 random stars at random positions and sizes. The power of loops + drawing.
Stretch — gradients
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, "#ff7e5f");
gradient.addColorStop(1, "#feb47b");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 400);
A vertical sunset gradient (orange to peach).
createLinearGradient(x1, y1, x2, y2) makes a
gradient from one point to another. addColorStop(t, color) places a color at position t (0 to 1).
Then use the gradient as a fillStyle.
Stretch — drawing with the mouse
You can draw with mouse events. Add to your script:
let drawing = false;
canvas.addEventListener("mousedown", () => drawing = true);
canvas.addEventListener("mouseup", () => drawing = false);
canvas.addEventListener("mousemove", (event) => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = "black";
ctx.fill();
});
Click and drag on the canvas. You’re drawing. A tiny paint program in 10 lines.
The getBoundingClientRect() business is needed
because the mouse position is relative to the page,
not the canvas. Subtract the canvas’s left/top to get
the canvas-relative position.
Stretch — clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
Erases everything. Useful for animations (next session).
Add a button to clear:
<button id="clearBtn">Clear</button>
<script>
document.querySelector("#clearBtn").addEventListener("click", () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
</script>
Extension — save as image
const dataURL = canvas.toDataURL("image/png");
// Open in new tab:
window.open(dataURL);
toDataURL() returns the canvas as an encoded image.
Visit the URL in a new tab to download. Real “save
my drawing” feature.
Wrap-up
Before we leave, share with the room:
- For everyone — show your scene. What did you build?
- For the kids who used loops — what did you generate?
- Anyone try the mouse drawing? Did it feel like a real paint program?
- Did the Pygame mental model transfer cleanly?
Today you learned:
<canvas>— drawing surface in HTML.getContext("2d")— your drawing handle (ctx).ctx.fillRect,strokeRect— rectangles.ctx.arc— circles (and other arcs).ctx.moveTo+lineTo— lines and polygons.ctx.beginPath/fill/stroke— the path pattern.ctx.fillStyle,strokeStyle,lineWidth,font— visual properties.ctx.fillText— text on canvas.- Mouse coordinates with
getBoundingClientRect.
The Pygame mental model directly transfers. Different language, same drawing primitives, same coordinates. If you survived Phase 6, you can canvas.
Next week: canvas + animation + input = a real game in the browser.
If you missed this session
Open Thonny.
-
Build the canvas-test.html with the basic rectangle and circle.
-
Build the scene from Part A — sky, ground, sun, house.
-
Build your own scene — pick a subject from the list, use at least 6 shapes.
-
(Stretch) Try the mouse-draw feature.
About 30-45 minutes. By the end you should have a canvas scene of your own.
Stretch and extension ideas
- Gradients — linear and radial.
- Mouse drawing — paint program.
- Fill style with patterns or images.
- Shadow effects —
ctx.shadowBlur,shadowColor. - Save as image with
toDataURL. - Clear button to wipe and start over.
- Color picker — let user choose color.
- Brush size — slider for the pen size.
- Read MDN’s Canvas tutorial for the full API.
What’s next
Next week: animation + input = a real
mini-game in the browser. We use
requestAnimationFrame for the frame loop, listen
for keyboard events, and build something playable.
The Pygame patterns from Phase 6 transfer directly.
Session 13: Canvas mini-game
Session 13: Canvas mini-game
Phase 7 — Web · Session 13 of 17
What we’re learning today
Last week you drew on a canvas. Today you make it
move and respond. You’ll learn the browser’s
frame loop (requestAnimationFrame), handle
keyboard input, and build a complete mini-game —
a falling-things catcher (just like Phase 6 Session
5, but in the browser). By the end, you’ll have a
playable game in HTML that anyone with a browser can
load. Save high scores with localStorage (Session 11
callback).
You’ll need to remember from last time
- Canvas + ctx (Session 12).
addEventListener(Session 9).localStoragefor the high score (Session 11).- Phase 6 Sessions 1, 4, 5 — frame loop, keyboard input, collision. All directly transfers.
- The Session 9 counter app is the same shape
as a game, just slower: state lives in variables
(
count), events change the state (button clicks), and the screen is updated to match. A game does the same thing 60 times a second: state lives in variables (positions, score), events change it (key presses + the frame loop’s tick), and the canvas is redrawn to match. If you can build the counter from memory, you can build today’s game.
Part A: The browser’s frame loop
requestAnimationFrame
In Pygame, you used a while running: loop with
clock.tick(60). The browser has a different
mechanism: requestAnimationFrame.
The pattern:
function gameLoop() {
// 1. Update state
// 2. Clear and redraw
requestAnimationFrame(gameLoop); // schedule the next frame
}
requestAnimationFrame(gameLoop); // start the loop
requestAnimationFrame(fn) says “run fn on the
next animation frame” — typically about 60 times per
second. The function recursively schedules itself,
keeping the loop going.
It’s smarter than a while loop:
- Synced to the screen refresh (usually 60 Hz).
- Pauses when the tab is hidden (saves battery).
- Doesn’t block the page (other JS can run).
Compare to Pygame:
# Pygame
while running:
# update
# draw
clock.tick(60)
// Browser
function gameLoop() {
// update
// draw
requestAnimationFrame(gameLoop);
}
gameLoop();
Same shape, different mechanism.
A bouncing rectangle
Open Thonny. Save a new file as bouncing.html:
<!DOCTYPE html>
<html>
<head>
<title>Bouncing</title>
<style>
body { margin: 0; background: #1a1a2e; }
canvas { display: block; margin: 20px auto; background: #0f3460; }
</style>
</head>
<body>
<canvas id="game" width="600" height="400"></canvas>
<script>
const canvas = document.querySelector("#game");
const ctx = canvas.getContext("2d");
// State
let x = 100;
let y = 100;
let dx = 3;
let dy = 2;
const SIZE = 40;
function gameLoop() {
// Update
x += dx;
y += dy;
if (x < 0 || x > 600 - SIZE) dx = -dx;
if (y < 0 || y > 400 - SIZE) dy = -dy;
// Draw
ctx.fillStyle = "#0f3460";
ctx.fillRect(0, 0, 600, 400); // clear (paint background)
ctx.fillStyle = "#e94560";
ctx.fillRect(x, y, SIZE, SIZE);
requestAnimationFrame(gameLoop);
}
gameLoop();
</script>
</body>
</html>
Save. Open in browser. A pink box bounces around the canvas.
What’s happening:
- Same as Phase 6 Session 1’s bouncing rect, but in JavaScript.
- Update: change x and y, flip direction at edges.
- Draw: fill the canvas (clear), draw the box.
requestAnimationFrame(gameLoop)schedules the next frame.
The pattern is identical to Pygame. If you got through Phase 6, this is muscle memory.
Keyboard input
For continuous (held-key) input, track key state in an object:
const keys = {};
document.addEventListener("keydown", (event) => {
keys[event.key] = true;
});
document.addEventListener("keyup", (event) => {
keys[event.key] = false;
});
Then in your update:
if (keys["ArrowLeft"]) x -= 5;
if (keys["ArrowRight"]) x += 5;
event.key is the name of the key pressed:
"ArrowLeft", "ArrowRight", "ArrowUp",
"ArrowDown", " " (space), "a", "b", etc.
(In Pygame this was pygame.key.get_pressed().
Browser is more manual but the model is the same.)
Build it — a tiny controllable square
Replace the bouncing logic with player controls:
let x = 280;
let y = 350;
const SPEED = 6;
const keys = {};
document.addEventListener("keydown", (e) => keys[e.key] = true);
document.addEventListener("keyup", (e) => keys[e.key] = false);
function gameLoop() {
if (keys["ArrowLeft"] || keys["a"]) x -= SPEED;
if (keys["ArrowRight"] || keys["d"]) x += SPEED;
if (keys["ArrowUp"] || keys["w"]) y -= SPEED;
if (keys["ArrowDown"] || keys["s"]) y += SPEED;
// Stay on screen
if (x < 0) x = 0;
if (x > 600 - 40) x = 600 - 40;
if (y < 0) y = 0;
if (y > 400 - 40) y = 400 - 40;
// Draw
ctx.fillStyle = "#0f3460";
ctx.fillRect(0, 0, 600, 400);
ctx.fillStyle = "#e94560";
ctx.fillRect(x, y, 40, 40);
requestAnimationFrame(gameLoop);
}
gameLoop();
Save. Reload. Use arrow keys (or WASD) to move the square around. It stays on screen. Real controllable character.
Checkpoint: You have a canvas with a player sprite that responds to keyboard input. This is the natural stop point if class is cut short.
Part B: A falling-things catcher
Time to build a complete game. You built one in Phase 6 Session 5 (fruit catcher). Today’s is the same idea, in browser.
Build it
Save a new file as catcher.html:
<!DOCTYPE html>
<html>
<head>
<title>Catcher</title>
<style>
body {
margin: 0;
background: #1a1a2e;
color: white;
font-family: Arial, sans-serif;
text-align: center;
}
canvas {
display: block;
margin: 20px auto;
background: #0f3460;
}
h1 { margin-top: 20px; }
#status { font-size: 24px; }
</style>
</head>
<body>
<h1>Catch the falling boxes</h1>
<p id="status">Score: 0 • Lives: 3</p>
<canvas id="game" width="600" height="500"></canvas>
<script src="catcher.js"></script>
</body>
</html>
catcher.js:
const canvas = document.querySelector("#game");
const ctx = canvas.getContext("2d");
const status = document.querySelector("#status");
const WIDTH = 600;
const HEIGHT = 500;
const PADDLE_W = 80;
const PADDLE_H = 16;
const BLOCK_SIZE = 30;
const SPEED = 7;
let paddleX = WIDTH / 2 - PADDLE_W / 2;
let blocks = [];
let score = 0;
let lives = 3;
let gameOver = false;
const keys = {};
document.addEventListener("keydown", (e) => keys[e.key] = true);
document.addEventListener("keyup", (e) => keys[e.key] = false);
let spawnTimer = 0;
const SPAWN_INTERVAL = 60; // frames
function update() {
if (gameOver) return;
// Move paddle
if (keys["ArrowLeft"] || keys["a"]) paddleX -= SPEED;
if (keys["ArrowRight"] || keys["d"]) paddleX += SPEED;
if (paddleX < 0) paddleX = 0;
if (paddleX > WIDTH - PADDLE_W) paddleX = WIDTH - PADDLE_W;
// Spawn blocks
spawnTimer++;
if (spawnTimer >= SPAWN_INTERVAL) {
spawnTimer = 0;
blocks.push({
x: Math.random() * (WIDTH - BLOCK_SIZE),
y: -BLOCK_SIZE,
speed: 2 + Math.random() * 3
});
}
// Move blocks + check catches/misses
for (let i = blocks.length - 1; i >= 0; i--) {
const b = blocks[i];
b.y += b.speed;
// Check catch (rect collision)
if (
b.y + BLOCK_SIZE >= HEIGHT - PADDLE_H &&
b.y <= HEIGHT &&
b.x + BLOCK_SIZE >= paddleX &&
b.x <= paddleX + PADDLE_W
) {
blocks.splice(i, 1);
score++;
} else if (b.y > HEIGHT) {
blocks.splice(i, 1);
lives--;
if (lives <= 0) {
gameOver = true;
}
}
}
status.textContent = `Score: ${score} • Lives: ${lives}`;
}
function draw() {
// Clear
ctx.fillStyle = "#0f3460";
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Paddle
ctx.fillStyle = "#e94560";
ctx.fillRect(paddleX, HEIGHT - PADDLE_H, PADDLE_W, PADDLE_H);
// Blocks
ctx.fillStyle = "#f9d56e";
for (const b of blocks) {
ctx.fillRect(b.x, b.y, BLOCK_SIZE, BLOCK_SIZE);
}
// Game over text
if (gameOver) {
ctx.fillStyle = "white";
ctx.font = "60px Arial";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", WIDTH / 2, HEIGHT / 2);
ctx.font = "24px Arial";
ctx.fillText("Reload to play again", WIDTH / 2, HEIGHT / 2 + 50);
}
}
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
gameLoop();
Save. Reload. Use arrows (or A/D) to move the paddle. Catch the falling blocks. Don’t let too many fall off — you only have 3 lives.
You built a complete browser game. The exact same shape as Phase 6’s fruit catcher, in JavaScript
- Canvas.
A few details:
- Loop blocks backwards when removing
(
for (let i = blocks.length - 1; i >= 0; i--)) — modifying an array while iterating forwards causes index issues. Same gotcha as Phase 6. blocks.splice(i, 1)removes the item at indexi(one item).- Rect collision — the four
>=/<=checks are the standard “do these two rectangles overlap” test. - Status updated in the DOM every frame —
outside the canvas, the score lives in a regular
<p>tag. Mix DOM and canvas for game UI.
Add a high score with localStorage
Add to the top of catcher.js:
let highScore = parseInt(localStorage.getItem("catcherHigh") || "0");
Then in update, when game over:
if (lives <= 0) {
gameOver = true;
if (score > highScore) {
highScore = score;
localStorage.setItem("catcherHigh", highScore);
}
}
Update the status display:
status.textContent = `Score: ${score} • Lives: ${lives} • Best: ${highScore}`;
Save. Reload. Play. Lose. Your high score is saved. Reload. The high score is still there.
parseInt(localStorage.getItem(...) || "0") — load
as integer. || "0" defaults to “0” if no saved
score yet.
Stretch — restart on R
Add to your event listeners:
document.addEventListener("keydown", (event) => {
if (event.key === "r" && gameOver) {
// Reset everything
score = 0;
lives = 3;
blocks = [];
gameOver = false;
}
});
Press R after game over to restart. No reload needed.
Stretch — bombs
Some falling things should be bombs — catching one costs a life:
// In spawn:
const isBomb = Math.random() < 0.2; // 20% chance
blocks.push({
x: Math.random() * (WIDTH - BLOCK_SIZE),
y: -BLOCK_SIZE,
speed: 2 + Math.random() * 3,
isBomb: isBomb
});
// In catch handling:
if (catch logic) {
if (b.isBomb) {
lives--;
} else {
score++;
}
blocks.splice(i, 1);
}
// In draw:
ctx.fillStyle = b.isBomb ? "#000000" : "#f9d56e";
ctx.fillRect(b.x, b.y, BLOCK_SIZE, BLOCK_SIZE);
Now you have to avoid catching bombs. Real gameplay tension.
Extension — speed up over time
Increase fall speed each catch:
// On catch:
score++;
// Slightly increase difficulty
// (would need to modify SPAWN_INTERVAL or speeds globally)
Or shorten SPAWN_INTERVAL:
let spawnInterval = 60;
// On catch:
spawnInterval = Math.max(20, spawnInterval - 1);
(Use spawnInterval as a variable instead of a
const.)
Wrap-up
Before we leave, share with the room:
- For everyone — show your catcher. What’s your high score?
- Did the Pygame patterns transfer cleanly? Any surprises?
- Did seeing the high score persist after a reload feel satisfying?
- For the kids who added bombs — how did the gameplay change?
Today you learned:
requestAnimationFrame— the browser’s frame loop.- Tracking key state with
keydown/keyuplisteners + akeysobject. - Rect collision with the four-comparison pattern.
- Backwards iteration when removing from arrays.
- DOM + canvas working together (canvas for game graphics, DOM for UI text).
- High score persistence with localStorage.
You built a real browser game. Open it in any browser, on any computer with internet. Anyone can play. That’s the web’s superpower.
Next week: fetch + JSON — talking to APIs across the internet. Then GitHub Pages. Then your milestone.
If you missed this session
Open Thonny.
-
Build the bouncing rectangle from Part A. See it bounce.
-
Add keyboard control. Move the square with arrows.
-
Build the catcher game from Part B.
-
(Stretch) Add high score persistence.
About 60-90 minutes — this is a substantial session.
Stretch and extension ideas
- Restart on R — reset state, no reload needed.
- Bombs that lose a life on catch.
- Speed up over time.
- Multiple block types — different colors, point values, sizes.
- Particle effects — small explosion when a block is caught.
- Sound —
new Audio("...mp3").play()on catch. - Mobile controls — touch events for tablets/ phones.
- Pause with P or Escape.
- Save the game state to localStorage so you can resume after a reload (advanced).
- Push to GitHub — your game becomes a public repo.
What’s next
Next week: fetch and JSON — JavaScript talking to the internet. We hit a public API, get data back as JSON, and display it on the page. Random dog photos, random jokes, random whatever an API offers.
Session 14: Fetch + JSON — talking to the internet
Session 14: Fetch + JSON — talking to the internet
Phase 7 — Web · Session 14 of 17
What we’re learning today
Your JS lives in your browser. The internet is full
of services that respond to requests with data —
weather, jokes, dog photos, sports scores, almost
anything. Today you’ll learn the fetch API to
ask for data, async/await to wait for the
response, and JSON to read what you get back.
By the end you’ll have a page that fetches random
content from a public API.
This is the web’s actual superpower — your tiny JavaScript can talk to anything online.
You’ll need to remember from last time
- JavaScript syntax (Session 8).
- DOM manipulation (Session 9).
JSON.parse/JSON.stringify(Session 11).- Functions and arrow functions (Session 8).
Part A: Fetching data
What’s an API?
An API (Application Programming Interface) is a URL you can request data from. Visit the URL — you get back data, usually in JSON format. Examples:
https://dog.ceo/api/breeds/image/random→{"message": "https://...", "status": "success"}— random dog photo URL.https://catfact.ninja/fact→{"fact": "...", "length": 79}— random cat fact.https://official-joke-api.appspot.com/random_joke→{"setup": "...", "punchline": "..."}— random joke.
These are free public APIs. No login, no payment, just request and respond. There are thousands of them.
Try one in the browser. Visit
https://catfact.ninja/fact. You see raw JSON. The
data is there. Now we make JS request it from your
page.
fetch(url)
The basic call:
fetch("https://catfact.ninja/fact")
This sends a request. But — wait, did anything happen? Yes, but it takes time. Network requests are asynchronous — your code keeps running while the data is on its way.
fetch returns a Promise — an object that says
“I’ll have the data eventually.” To get the data,
you wait for the promise.
Async / await — the modern way
The cleanest pattern:
async function getCatFact() {
const response = await fetch("https://catfact.ninja/fact");
const data = await response.json();
console.log(data.fact);
}
getCatFact();
What’s happening:
async function— declares the function as asynchronous. Means it can useawaitinside.await fetch(...)— wait for the network response.response.json()— convert the response body to a JavaScript object (parse the JSON). This also takes time, so weawaitit too.- Then
data.factis the value we want.
Try it. Open cat.html in Thonny:
<!DOCTYPE html>
<html>
<head>
<title>Cat facts</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 60px auto;
padding: 20px;
text-align: center;
}
button {
font-size: 18px;
padding: 10px 20px;
cursor: pointer;
}
#fact {
font-size: 22px;
margin: 30px 0;
min-height: 50px;
}
</style>
</head>
<body>
<h1>🐱 Random Cat Fact</h1>
<p id="fact">Click the button to learn something.</p>
<button id="btn">Get a fact</button>
<script>
const btn = document.querySelector("#btn");
const fact = document.querySelector("#fact");
async function loadFact() {
const response = await fetch("https://catfact.ninja/fact");
const data = await response.json();
fact.textContent = data.fact;
}
btn.addEventListener("click", loadFact);
</script>
</body>
</html>
Save. Open in browser. Click the button. A random cat fact appears. Click again. New fact.
You just made your page talk to the internet.
Error handling
What if the network is down, or the API is broken?
Wrap with try/catch:
async function loadFact() {
try {
const response = await fetch("https://catfact.ninja/fact");
const data = await response.json();
fact.textContent = data.fact;
} catch (error) {
fact.textContent = "Couldn't load a fact. Try again?";
console.error(error);
}
}
Same try/except pattern as Python (Phase 3 Session
13), with try/catch instead.
Always include error handling for fetches. Networks fail.
What does the data look like?
Add a console.log(data) line to see the full
structure:
const data = await response.json();
console.log(data);
fact.textContent = data.fact;
In the DevTools console you see the full object —
a dictionary-like thing with fact and length
keys. JSON is just objects and arrays.
Different APIs return different shapes. Always explore the data first before using it.
Try a dog photo API
Open dog.html:
<!DOCTYPE html>
<html>
<head>
<title>Dog photos</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 20px;
}
img {
max-width: 500px;
max-height: 500px;
margin: 20px 0;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>🐶 Random Dog</h1>
<button id="btn">New dog</button>
<div>
<img id="dogImg" src="" alt="A dog">
</div>
<script>
const btn = document.querySelector("#btn");
const dogImg = document.querySelector("#dogImg");
async function loadDog() {
try {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const data = await response.json();
dogImg.src = data.message;
} catch (error) {
console.error(error);
}
}
btn.addEventListener("click", loadDog);
loadDog(); // load on page open
</script>
</body>
</html>
Save. Open. A random dog photo loads automatically. Click the button for another. A new dog every time.
The Dog API returns {"message": "<url>", "status": "success"}. data.message is the URL of the dog
photo. We assign it to the <img src>.
Checkpoint: You have a page that fetches data from a public API and displays it. This is the natural stop point if class is cut short.
Part B: Combine multiple fetches
Time to build something more interesting.
Joke + dog combo
A page that shows a random dog photo and a random joke side by side, with one button to refresh both.
Save as combo.html:
<!DOCTYPE html>
<html>
<head>
<title>Dogs & Jokes</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
text-align: center;
}
.row {
display: flex;
gap: 20px;
margin: 30px 0;
align-items: center;
}
.col {
flex: 1;
}
img {
max-width: 100%;
max-height: 400px;
border-radius: 8px;
}
button {
font-size: 18px;
padding: 12px 24px;
cursor: pointer;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
}
#joke {
font-style: italic;
font-size: 20px;
}
</style>
</head>
<body>
<h1>Dog + Joke combo</h1>
<button id="btn">Refresh</button>
<div class="row">
<div class="col">
<img id="dogImg" src="" alt="A random dog">
</div>
<div class="col">
<p id="joke">Click the button.</p>
</div>
</div>
<script>
const btn = document.querySelector("#btn");
const dogImg = document.querySelector("#dogImg");
const joke = document.querySelector("#joke");
async function loadDog() {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const data = await response.json();
dogImg.src = data.message;
}
async function loadJoke() {
const response = await fetch("https://official-joke-api.appspot.com/random_joke");
const data = await response.json();
joke.textContent = `${data.setup} — ${data.punchline}`;
}
async function refresh() {
try {
await Promise.all([loadDog(), loadJoke()]);
} catch (error) {
console.error(error);
}
}
btn.addEventListener("click", refresh);
refresh();
</script>
</body>
</html>
Save. Open. A dog photo and a joke load. Click “Refresh.” Both update.
What’s new:
Promise.all([a, b])— runs both fetches in parallel and waits for both to finish. Faster than waiting for one, then the other.
List response — multiple results
Some APIs return arrays. Try the JokeAPI:
const response = await fetch("https://official-joke-api.appspot.com/jokes/ten");
const jokes = await response.json(); // array of 10 jokes
console.log(jokes.length); // 10
Then loop and add to the page:
const list = document.querySelector("#jokeList");
list.innerHTML = ""; // clear
for (const j of jokes) {
const li = document.createElement("li");
li.textContent = `${j.setup} — ${j.punchline}`;
list.appendChild(li);
}
Renders 10 jokes as a list.
Stretch — loading state
Network requests aren’t instant. While waiting, show a loading message:
async function loadDog() {
dogImg.src = "";
dogImg.alt = "Loading...";
try {
const response = await fetch("https://dog.ceo/api/breeds/image/random");
const data = await response.json();
dogImg.src = data.message;
dogImg.alt = "A random dog";
} catch (error) {
dogImg.alt = "Failed to load";
}
}
Or a spinner / “loading…” text. Real apps always show feedback.
Stretch — explore other APIs
Public APIs to try:
- Bored API —
https://www.boredapi.com/api/activity→ returns a random activity to do. - Pokemon —
https://pokeapi.co/api/v2/pokemon/charizard→ returns Pokemon data. Try different names. - NASA APOD —
https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY→ astronomy picture of the day. - Open-Meteo — weather data (more complex).
- OpenLibrary — book search.
- Star Wars API —
https://swapi.dev/api/people/1→ character data.
For each: visit the URL in your browser to see the JSON, then write JS to fetch and display it.
Stretch — search interface
Build a “search Pokemon” page:
<input id="nameInput" type="text" placeholder="Pokemon name">
<button id="searchBtn">Search</button>
<div id="result"></div>
async function search() {
const name = nameInput.value.trim().toLowerCase();
if (!name) return;
try {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`);
const data = await response.json();
result.innerHTML = `
<h2>${data.name}</h2>
<img src="${data.sprites.front_default}" alt="${data.name}">
<p>Height: ${data.height}, Weight: ${data.weight}</p>
`;
} catch (error) {
result.textContent = "Pokemon not found.";
}
}
A real search interface. Type “pikachu”, “charmander”, “mew” — different Pokemon load.
The ${name} in the URL is template literal
substitution — building dynamic URLs from input.
Extension — display saved API responses
Combine fetch with localStorage. Save the last 10 fetched items. Display history.
Extension — error handling polish
Try cutting your wifi. Click the button. Right now, the error goes to the console — silent failure for the user. Fix it: show a friendly error message in the UI when fetch fails.
Wrap-up
Before we leave, share with the room:
- For everyone — show what you fetched. Cat facts? Dogs? Jokes?
- Did the wait-for-the-response delay feel noticeable?
- For kids who explored other APIs — show the weirdest one you found.
- For the Pokemon-searcher kids — what’s your favorite Pokemon’s name?
Today you learned:
- APIs — URLs that return data.
fetch(url)— JS’s HTTP request.async/await— the modern way to wait for async results.response.json()— parse the response body as JSON.try/catch— error handling for failures.Promise.all([...])— parallel fetches.- Template literals in URLs for dynamic requests.
Your JavaScript can now talk to the world. Any public API. Any data on the internet. That’s the foundation of every modern web app — Twitter, Instagram, weather apps, news sites — all hit APIs behind the scenes.
Next week: GitHub Pages. Your work becomes publicly accessible with a real URL anyone can visit. After that: your milestone.
If you missed this session
Open Thonny.
-
Build the cat-fact page from Part A. Click the button, see facts.
-
Build the dog-photo page. Click for new dogs.
-
Build the combo (dog + joke).
-
(Stretch) Try one other API from the list.
About 30-45 minutes. By the end you should have at least one page that fetches and displays API data.
Stretch and extension ideas
- Loading state — show “Loading…” while waiting.
- Error handling polish — friendly UI on failure.
- Multiple APIs combined in one page.
- Search interface — input + button + display.
- Save fetched data to localStorage.
- Display fetch history.
- A weather widget — fetch from Open-Meteo.
- A news headline ticker — fetch periodically.
- Auto-refresh every N seconds with
setInterval.
What’s next
Next week: GitHub Pages. All this work you’ve been building? Push it to GitHub, flip a setting, and anyone with the link can use it. Your homepage, your todo app, your canvas game, your fetch experiments — all hostable for free.
Session 15: GitHub Pages — host your work
Session 15: GitHub Pages — host your work
Phase 7 — Web · Session 15 of 17
What we’re learning today
You’ve built websites. They run on your computer.
Today they run on the internet — anyone with the
URL can visit, no install required. GitHub Pages
hosts static websites directly from your GitHub repos,
free, with custom URLs at <username>.github.io. By
the end of class, your personal homepage will be at a
real URL you can share with anyone.
This is the moment your code becomes shareable to the world.
You’ll need to remember from last time
- Phase 6 Session 7 — Git remote, push to GitHub. PATs.
- Your homepage from Session 6 (or any other HTML/JS project worth deploying).
- The terminal —
cd,git.
Part A: Deploy your homepage
What’s GitHub Pages?
GitHub Pages is a free static hosting service built into GitHub. You push HTML/CSS/JS to a repo, flip a setting, and GitHub serves your site at a public URL.
Limits:
- Static only. No backend code (no Python/Node on the server). Just HTML, CSS, JS, images.
- Public for free (private with a paid plan).
- ~1 GB total per site (huge for our scale).
- Soft bandwidth limit of ~100 GB/month (also huge for personal sites).
For everything we’ve built in Phase 7 — homepages, todos, canvas games, fetch demos — GitHub Pages is perfect.
Step 1 — Push your homepage repo
If you don’t already have your homepage in a GitHub repo:
- Make a folder for it on your machine.
- Inside, you should have at least
index.htmlandstyles.css. - From the terminal:
$ cd ~/projects/homepage # or wherever yours is $ git init $ git add . $ git commit -m "Initial homepage" - On GitHub, create a new repo (let’s say named
homepage). - Connect and push:
$ git remote add origin https://github.com/YOUR-USERNAME/homepage.git $ git push -u origin main
(All of this is Phase 6 Session 7. Refresher if needed.)
Step 2 — Enable Pages
- Go to your repo on GitHub.
- Click Settings (top of the repo).
- In the left sidebar, click Pages.
- Under Source, pick Deploy from a branch.
- Under Branch, pick main and / (root).
- Click Save.
GitHub now starts deploying your site. Takes 1-2 minutes the first time.
Step 3 — Visit your URL
Wait a moment, then refresh the Pages settings page. At the top you’ll see:
Your site is live at https://YOUR-USERNAME.github.io/homepage/
Click it. Your homepage loads at a real public URL.
That URL is shareable. Send it to anyone — parents, friends, classmates. They can visit on their phone, their laptop, anywhere with internet. Your code is on the internet.
Step 4 — Make a change, push, watch deploy
The whole point: your site updates automatically when you push.
- Edit your
index.html. Change a heading or add a line. - Save. Reload your local file to verify.
- From terminal:
$ git add . $ git commit -m "Update homepage" $ git push - Wait 30-60 seconds.
- Reload the public URL. Your change is live.
This is the deployment loop. Edit → commit → push → live within a minute.
A note on the URL pattern
GitHub Pages URLs follow a pattern:
https://USERNAME.github.io/REPO-NAME/— for any normal repo.https://USERNAME.github.io/— for a special repo namedUSERNAME.github.io. This is your root GitHub Pages site.
If you want your homepage at <username>.github.io
(no /repo-name/), name your repo exactly
USERNAME.github.io (replace USERNAME with yours).
For now, keep it simple — homepage repo, URL ends
in /homepage/.
Checkpoint: Your homepage is live at a public URL. You can share the link. This is the natural stop point if class is cut short.
Part B: Deploy more projects
Each of your Phase 7 projects can become its own public URL.
Pick a project
Each kid: pick another HTML/CSS/JS project from Phase 7 that you’d like to share:
- Todo list (Sessions 10-11)
- Canvas game (Session 13)
- Fetch demo (Session 14) — cat facts, dog photos, joke combo, Pokemon search
- Anything else you’ve built
Same process
- Make sure the project is in its own folder with
at least
index.html. git init, commit, push to a new GitHub repo.- Enable Pages on that repo.
- Visit the URL.
15 minutes per project once you have the rhythm.
Add links between your sites
Your homepage can link to your other deployed projects — making your portfolio cohesive.
In your homepage’s HTML:
<section>
<h2>My projects</h2>
<ul>
<li><a href="https://USERNAME.github.io/todo/">Todo app</a></li>
<li><a href="https://USERNAME.github.io/catcher/">Catcher game</a></li>
<li><a href="https://USERNAME.github.io/dog-jokes/">Dogs & jokes</a></li>
</ul>
</section>
Push the homepage. Now your homepage URL is the hub for everything you’ve built.
Stretch — README on each repo
Each repo benefits from a README.md with:
- What the project does.
- Live URL.
- Screenshot.
- How to run locally.
When you visit github.com/USERNAME/REPO, the README
shows. Real-developer professionalism.
Stretch — custom domain (mention only)
You can use a custom domain (like myname.com)
instead of username.github.io/.... Requires
buying a domain (~$10/year) and configuring DNS.
If you have a domain, GitHub’s Pages settings has a “Custom domain” field. Out of scope for class but worth knowing about.
Stretch — GitHub Pages from a subfolder
If you want to deploy only part of a repo (e.g.,
your dist/ folder), the GitHub Pages settings let
you pick the source folder. Useful for build-step
sites — but not relevant for our vanilla-JS
projects.
Extension — share your URLs in the class
Make a class shared list. Everyone adds their URLs. Visit each other’s projects. Real audience for your work.
Extension — push existing repos to Pages
Every repo from Phase 6 (your Pong, fruit catcher variants, etc.) — those are Pygame games, unfortunately not deployable to Pages (they need Python on the user’s machine).
But if you also rebuilt any of those games in canvas (Session 13), you can deploy those.
For Pygame games, you can:
- Deploy a page about the game with screenshots and a download link.
- Convert to canvas (significant work).
- Use a tool like Pyodide or Brython to run Python in the browser (advanced).
For now, focus on the HTML/CSS/JS projects.
Wrap-up
Before we leave, share with the room:
- For everyone — show your live URL. Type it into a browser. Watch it load.
- Have someone else try yours on their machine / phone. It works.
- Anyone deploy more than one project? Show your homepage as the hub.
- Did seeing your URL on the internet feel like a moment?
Today you learned:
- GitHub Pages — free static hosting from your GitHub repos.
- Enable in Settings → Pages.
- URL pattern:
<username>.github.io/<repo>/ - Update via git push — deployments are automatic.
- Special repo name for the root URL.
This is a real production deployment workflow.
Every push to main deploys to the public URL.
Tens of millions of sites use this exact pattern
(it’s also what powers many open-source
documentation sites — like the one you’re reading
right now, hosted on GitHub Pages).
You can now share your work as URLs. That’s the web’s superpower. Your code, on the internet, free, available globally, without server maintenance.
Next two weeks: your milestone web project. Your design, your code, hosted on GitHub Pages by demo day.
If you missed this session
You need a GitHub account (Phase 6 Session 7) and
a project with at least an index.html.
-
Push your project to a new GitHub repo.
-
In repo Settings → Pages, set source to “main” branch, “/ (root)” folder.
-
Wait 1-2 minutes.
-
Visit
https://USERNAME.github.io/REPONAME/. -
(Stretch) Deploy more projects.
About 30-45 minutes. By the end you should have at least one URL you can share.
Stretch and extension ideas
- Deploy multiple projects.
- Cross-link your projects from your homepage.
- Add READMEs to each repo.
- Try
<username>.github.ioas the URL — needs a special repo name. - Custom domain if you have one.
- Set up an
about.htmlandprojects.htmlas separate pages in your homepage repo. - Verify mobile — visit your URL on a phone. Does it look right?
- Track visitors with a simple analytics service (advanced — Plausible, Simple Analytics).
What’s next
Next week: milestone planning + work day 1. You plan and start your web project. Your design, your code. The week after, you finish and demo your live URL to the class.
Bring an idea or two. Or come empty-handed — we have a seed list ready.
Session 16: Milestone project work day 1
Session 16: Milestone project work day 1
Phase 7 — Web · Session 16 of 17
What we’re learning today
Today is your day. You’ll plan a web project — your design, your code, hosted on GitHub Pages by demo day — and start building it. Next week you’ll finish it and demo your live URL to the class.
This is your seventh milestone project. You have every tool you need.
You’ll need to remember from last time
- HTML structure — semantic tags.
- CSS — the box model, Flexbox.
- JavaScript — DOM, events, fetch, localStorage.
- Canvas if your project includes graphics.
- GitHub Pages — for hosting.
- Git — commit as you go; push to deploy.
Part A: Plan your project
The plan
Take a piece of paper or open a blank text file. Answer these eight questions:
-
What’s the project? (One sentence. “A recipe collector.” “A canvas paint app.” “A weather widget.”)
-
What does the user do with it? (How do they interact?)
-
What HTML elements does it need? (Forms? Lists? Canvas? Image grid?)
-
What does it look like? (Sketch the layout on paper. Where’s the navbar, content, footer?)
-
What’s the JavaScript doing? (Click handlers? DOM updates? Fetch from an API?)
-
Does it need persistence? (localStorage? What state?)
-
What’s the simplest version? (Build this FIRST. Just the core. No fancy features.)
-
What’s one stretch feature? (After core works.)
Phase 7 requirements
Your milestone must:
- Have HTML, CSS, and JavaScript (use all three).
- Be hosted on GitHub Pages by the demo — with a public URL you can share.
- Run without crashing under normal use.
- Look styled (not raw HTML — at minimum, custom font + color).
Bonus:
- Uses fetch (calls a public API).
- Uses localStorage (persists state).
- Uses canvas (graphics or game).
- Looks decent on mobile (resize browser to test).
If you don’t have an idea
Pick one and modify:
Content / portfolio sites:
- A recipe site — your favorite recipes, with photos.
- A book/movie review site — list, ratings, notes.
- A multi-page personal site — about, blog, projects, contact.
- A fan site for a game/anime/band — info, pictures, links.
Interactive apps:
- A flashcard study app — questions on cards, flip to see answer.
- A trivia game — multiple choice, score tracking.
- A simple calculator — buttons, display.
- A unit converter — pick from/to units.
- A countdown timer / pomodoro — start, pause, reset.
- A tip calculator — bill amount + tip percent.
- A password generator — length, character types.
Data-driven (uses fetch):
- A random Pokemon explorer — search by name, show stats and image.
- A weather widget — fetch current weather for a city.
- A trivia quiz — fetch questions from trivia API.
- A NASA picture of the day — fetch and display.
- A dad joke generator — fetch a new one each click.
Canvas / drawing:
- A paint app — pick color, draw, save.
- A pixel art editor — grid of cells, click to color.
- A simple drawing pad with brush options.
Canvas games:
- A catcher game like Session 13 — your theme.
- Snake — grid, snake grows, eats food.
- Breakout / brick breaker — paddle, ball, bricks.
- A flappy bird clone — gravity, tap to flap, pipes.
- A dodger game — avoid falling things, survive as long as possible.
- A memory match — flip cards, match pairs.
Collaborative / shareable:
- A shared list with copyable URL (data in URL fragment).
- A tip sheet for some topic you’re an expert on.
- A class profile collection — each student has a card.
Pick one. Spend two minutes. Don’t overthink.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question.
Set up the project
$ cd ~ # or wherever
$ mkdir my_project
$ cd my_project
$ touch index.html # create empty file
$ git init
$ git add index.html
$ git commit -m "Initial project setup"
Push to GitHub now (so deployment works later):
- Create a new public repo on GitHub.
- Connect:
$ git remote add origin https://github.com/USERNAME/my_project.git $ git push -u origin main - Enable GitHub Pages in Settings → Pages.
Now any push deploys immediately. Build with the URL ready to go.
Build the simplest version first
Look at your answer to question 7. Build that first. Get something on screen. Get one feature working. Iterate.
For each significant change:
- Make the change.
- Open
index.htmlin browser. Test. git add+git commit -m "...".- (Optional, but encouraged)
git push— deploy the in-progress version. - Move to the next thing.
Use what you’ve learned
- Semantic HTML —
<header>,<main>,<footer>. - CSS variables for a centralized palette.
- Flexbox for layout.
- DOM manipulation with
querySelector,addEventListener. - Forms with
preventDefault. - localStorage for any state worth keeping.
- Canvas if your project is graphics-heavy.
- Fetch if your project pulls data from APIs.
The complete list of patterns is in Sessions 1-15. Skim them when stuck.
Wrap-up
Last 5 minutes: each of you, in one sentence, tell the room one thing you got working today.
Bring your project (and the deployed URL!) next week. We’ll finish, then demo.
If you got far, push to GitHub and verify the URL works before next week. The demo requires the URL to be live.
If you missed this session
Open Thonny.
-
Spend 10-15 minutes answering the eight planning questions.
-
Set up the project folder +
index.html+git init+ commit. -
Push to a new GitHub repo. Enable Pages.
-
Build the simplest version. Commit and push as you go.
About 60-90 minutes total. By next week you should have:
- An
index.htmlwith at least the page structure. - One core feature working.
- A live GitHub Pages URL.
- Several Git commits.
If you don’t have an idea, pick from the seed list.
Stretch and extension ideas
- Add fetch if you didn’t plan on it.
- Add localStorage for any state.
- Add canvas for a visual element.
- Make it responsive with
@mediaqueries. - Multi-page site — about, projects, etc.
- README on the repo with description and live URL.
- Custom favicon in the browser tab.
What’s next
Next week is the last session of Phase 7. You’ll have time to finish, polish, and demo your live URL to the class. Each person gets 3-5 minutes. Bring a working app at a public URL.
Session 17: Milestone project work day 2 + demo day
Session 17: Milestone project work day 2 + demo day
Phase 7 — Web · Session 17 of 17
What we’re learning today
Today is the last session of Phase 7. First half: finish your project — fix bugs, add the last features, push the polished version. Second half: demo your live URL to the class. Anyone with internet can visit; your audience is the world.
You’ll need to remember from last time
- Your project plan from last week.
- Whatever you got working last week.
- Your buddy for testing.
- Git + GitHub Pages — push to deploy.
Part A: Final polish
You have about 35 minutes to finish.
What “finished” means
Same three rules as always:
- It runs without crashing under normal use.
- It does what your plan said — at least the simplest version.
- You can explain how it works.
If your project doesn’t meet all three, focus on getting there before adding anything new.
Phase 7 specific polish
- Verify the live URL works. Push your latest, wait 30 seconds, visit the URL, click around. Don’t assume — verify.
- Test on a phone if you can. Open the URL on
a phone or resize your browser window. Looks
ok? If text is unreadable or buttons too small,
add a
@media (max-width: 600px)rule. - Add a meaningful page title — `
… ` shows in the browser tab. - Add a favicon — a small icon in the tab.
Drop a
favicon.pngnext toindex.htmland link from<head>. - Console clean — open DevTools console. Any errors? Fix or suppress.
- README on the repo — what the project does, the live URL, controls or instructions.
- All committed and pushed —
git statusshould say “working tree clean” before demo.
Buddy test
About 15 min in, swap with your buddy. They visit your URL on their machine. They click around. You do the same.
For Phase 7 specifically: send the URL via chat or paper. Nothing makes a site feel real like visiting it on a different machine.
Notice things they get wrong (control confusion, unclear next step). Real UX testing.
Push your final version
$ git status # see what's pending
$ git add .
$ git commit -m "Final polish"
$ git push
Wait 30-60 seconds. Visit the URL. Verify everything looks right.
Checkpoint: Your project runs at a live URL, the basics work, and you can demo it. This is the natural stop point if class is cut short — but today, demo time is next.
Part B: Demo day
Each person gets 3-5 minutes.
How a demo works
When it’s your turn:
- Show your URL. Open it on the projector. Walk us through the page.
- Use it live. Click buttons. Type things. Let us see it actually working.
- Have a classmate visit your URL on their machine. Real test of “shareable to anyone.”
- Tell us about it. What is it? Who is it for?
- Show your GitHub repo. Open the URL in a
browser. Show your
git log. - Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected.
- Take one question.
Make sure your URL is shareable
Type your URL in the chat / on a piece of paper. Other students should visit during your demo. Real audience reaction.
After everyone demos
Mr. Eric will say a few specific things about each project. Then we celebrate Phase 7 completion.
What you accomplished
- Seventh milestone project. Shipped.
- A real live URL anyone can visit.
- HTML + CSS + JavaScript all combined into one app.
- Hosted on the internet — your code reaches beyond your machine.
- Public GitHub repo with the source code visible to anyone.
- You finished Phase 7. The web is yours.
What’s next: Phase 8 — Flask (web apps)
Phase 8 is the back-end — Python on a server that responds to web requests, generates HTML, talks to a database, and powers real multi-user apps.
What’s different:
- Server-side code. Python runs on a server, not in the browser. Your code processes requests, generates responses.
- Persistent data across users. A real database (we’ll use SQLite — simple, file-based). Multiple users can share data.
- User accounts and login — finally possible with a backend.
- Routes — URLs that map to Python functions.
/login,/profile,/dashboard. - Templates — HTML files with placeholders Python fills in.
You’ll build:
- A simple Flask app (Hello world).
- A multi-page server-rendered site.
- A note-taking app with user accounts and database storage.
- A milestone full-stack app.
What’s the same:
- Python (Phase 3-4 + Phase 6).
- HTML and CSS (Phase 7).
- Git + GitHub for source control.
Phase 8 is the last phase of the curriculum. After it, you’ll have built apps end-to-end — front-end (browser) AND back-end (server) — like a real software engineer.
Bring your machine and your enthusiasm. See you in Phase 8.
If you missed this session
Two cases:
Missed only the demo half: Show your live URL to Mr. Eric at the start of next week’s class. Same kind of feedback you’d have gotten in the demo.
Missed the whole session: No big deal. Finish your project at home using your Session 16 plan, push it to GitHub Pages, and bring the URL next week. Or just join us in Phase 8 — you’ve already shipped enough this phase to be proud.
Stretch and extension ideas
If you have time after demos, or want to keep building at home:
- Polish more. Add features, more sound, better art, smoother interactions.
- Share your URL widely. Family, friends, anyone. Get real feedback.
- Add analytics — see how many people visit. Plausible Analytics or Simple Analytics are privacy-respecting and free for small sites.
- Multiple deployed projects — your portfolio grows.
- Try a static site generator like Jekyll (built into GitHub Pages — out of curriculum scope but accessible).
- Save a copy of every milestone project. They are yours. Seven so far.
What’s next
Phase 8 — Flask. The back-end. See you there.
Phase 8 — Flask (web apps)
You can build games. You can build websites. You can make pages that talk to APIs and store data in the browser. Now you build the back-end — a server that responds to web requests, talks to a database, manages user accounts, and powers real multi-user applications.
This is the last phase of the curriculum. By the end, you’ll have built apps end-to-end — front-end (browser) AND back-end (server) — like a real software engineer.
What this phase is
Fourteen sessions on building web apps with Flask — a small, beginner-friendly Python web framework. You’ll learn how the browser-server conversation works, how to handle forms, how to store data in a real database, how to add user accounts, and how to deploy your app so it’s accessible to anyone with internet.
What you’ll learn
| Session | Idea | What’s new |
|---|---|---|
| 1 | Welcome to Flask — first server | The request/response cycle, install, hello world |
| 2 | Routes and URL parameters | Multiple routes, dynamic URLs |
| 3 | Templates — Jinja2 | Variables, loops, conditionals in HTML |
| 4 | Static files + base templates | CSS, images, template inheritance |
| 5 | Forms and POST requests | request.form, GET-then-POST |
| 6 | Sessions and flash messages | session, cookies, user feedback |
| 7 | SQLite — your first database | What a database is, the sqlite3 module |
| 8 | Database in Flask — CRUD | SELECT/INSERT/UPDATE/DELETE in routes |
| 9 | User accounts | Signup, login, logout, password hashing |
| 10 | Notes app — foundation | Two-table schema, per-user filter, list + create |
| 11 | Notes app — polish | View, edit, delete, multi-user + security demo |
| 12 | Deployment | Real public hosting (PythonAnywhere) |
| 13 | Milestone day 1 | Plan + build your full-stack app |
| 14 | Milestone day 2 + demo | Finish + showcase + curriculum close |
What you’ll build
- Sessions 1-6: small Flask apps — greetings, page-per-route sites, form handlers, simple games.
- Sessions 7-9: apps with persistent data and user accounts.
- Sessions 10-11: a multi-user notes app — auth, database, full CRUD. Built as a class across two sessions so the per-user security pattern gets the room it deserves.
- Session 12: deploy that app to a public URL with real users (you and your classmates).
- Sessions 13-14: your full-stack milestone — your design, deployed, anyone can sign up and use.
What you’ll need
- Python 3 — already installed on your machine from Phase 3.
- Flask — we’ll install in Session 1:
$ pip install flask sqlite3— built into Python.- A browser for testing.
- Thonny for editing.
- A PythonAnywhere account by Session 12 — free, set up together.
How sessions work
Same shape as before:
- Part A introduces a concept with a guided exercise.
- Part B is open practice or a project.
- Wrap-up to share what you did.
How the back-end is different
A few key shifts:
The server runs your code
In Phase 7, JavaScript ran in the browser — on the user’s computer. In Phase 8, Python runs on the server — your code (or, eventually, a code running in the cloud). Different runtime, different mental model.
Multiple users hit your code
A static page served by GitHub Pages handles millions of visitors with no special logic — every visitor sees the same files. A Flask app runs your Python on every request, so different users can see different things. Personalized pages. Logins. Per- user data. Real apps.
Data lives in a database
In Phase 7, browser localStorage held data — but per-user, per-browser. No sharing. No multi-device. No history.
A real database stores data centrally. A user logs in from any device, their data follows. Multiple users share a system. Real persistence.
URLs become functions
@app.route("/about") says “when someone visits
/about, run this Python function and return what
it produces.” Routes are the bridge between web URLs
and your code.
What’s the same
- Python. All your Phase 3-4 + Phase 6 Python knowledge applies. Flask is just a library.
- HTML and CSS. Phase 7. Templates produce HTML.
- Forms, JavaScript (optional). You can mix Phase 7 front-end skills into Flask templates.
- Git + GitHub. For source control, as before.
What we’re skipping
- SQLAlchemy / ORMs. Real production tools, but abstract away what’s happening. We’ll write raw SQL — clearer for learning.
- Blueprints, Flask extensions. Beyond the beginner needs.
- Async (asyncio). Out of scope.
- REST API design as a topic. We’ll touch JSON responses but not formal REST.
- Docker, Kubernetes, microservices, CI/CD pipelines. Real production has all of this. We’re learning Flask itself first.
- Testing. Phase 4 covered testing. Adding it to Flask is a natural next step but out of phase scope.
If you want to learn any of those after the curriculum, the foundation here makes it much easier.
A note about pacing
The back-end is quieter on the surface than Phases 6-7 — your screen often looks similar from session to session. But what’s behind each page changes fundamentally. Each session this phase, your server starts doing something it couldn’t do before.
Watch what your server gains, session by session:
- Sessions 1-4 — your server learns to talk. Hello world (1), then dynamic URLs (2), then rendering templates (3), then serving styles and shared layouts (4).
- Session 5 (forms) — the first time the user changes data on your server. Real conversation, not just reading.
- Session 6 (sessions + flash) — your server starts remembering who’s who across requests.
- Session 7 (database) — your first real persistent data. It survives restarts.
- Sessions 8-9 (CRUD + user accounts) — full read/write to the database, and “I have my own account on a thing I built.”
- Sessions 10-11 (notes app) — the integration moment. Multi-user. Per-user data isolation. Real CRUD. The shape of every modern web app.
- Session 12 (deployment) — your friends can sign up and use it.
Where to start
Session 1: Welcome to Flask sets up your first server.
When you’re stuck, the Getting unstuck appendix is the first place to go. The Glossary will grow as Phase 8 introduces back-end terms.
Welcome to the back-end. Last phase. Let’s go.
Session 1: Welcome to Flask — your first server
Session 1: Welcome to Flask — your first server
Phase 8 — Flask · Session 1 of 13
What we’re learning today
Until now your code has run on the user’s machine — Pygame games on their computer, web pages in their browser. Today you write code that runs on a server — a separate program that listens for requests from browsers and sends back responses. By the end of class, you’ll have a Flask server running on your machine, browsers can hit it, and you’ll see the request-response cycle live.
You’ll need to remember from last time
- Python — Phases 3, 4, 6.
- The terminal —
cd,python(Phase 4 Session 1). - HTML — Phase 7 Sessions 1-2.
- Browsers — Phase 7.
pip install— for installing Python packages.
Part A: The request-response cycle
What’s a server?
A server is a program that listens for requests from clients (usually browsers) and sends back responses. The conversation:
1. Browser: "GET https://example.com/about"
2. Server: reads the URL, decides what to send
3. Server: "200 OK, here's the HTML for the about page"
4. Browser: receives, displays the page
Every website works this way. When you visit
google.com:
- Your browser sends a request to Google’s servers.
- Google’s server looks at the URL, processes the request.
- The server sends back HTML (and CSS, JS, images, etc.).
- Your browser displays the page.
In Phase 7, we built only the client side — HTML/CSS/JS in the browser. Today we build the server side — code that listens for requests and decides what to send.
What’s Flask?
Flask is a Python library for building web servers. It handles the boring parts (listening on a port, parsing requests, sending responses) and lets you focus on what to do with each request.
A Flask server is just a Python program. You write Python functions that respond to specific URLs. When the browser hits that URL, Flask runs your function and sends the result back.
Install Flask
Open a terminal:
$ pip install flask
If you see “Successfully installed flask-…” you’re
good. If pip isn’t found, try pip3 or python -m pip install flask.
Your first server
Open Thonny. Save a new file as app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, web!"
if __name__ == "__main__":
app.run(debug=True)
Save. Run (Thonny’s run button, or python app.py
from terminal).
You’ll see something like:
* Running on http://127.0.0.1:5000
* Restarting with stat
* Debugger is active!
The server is running. Open a browser and visit:
http://127.0.0.1:5000
(Or http://localhost:5000 — same thing.)
You see: Hello, web!
That’s it. You wrote a server. Browser made a request, your Python code ran, the result came back.
Stop the server with Ctrl+C in the terminal.
What every line does
from flask import Flask
Import the Flask class.
app = Flask(__name__)
Create the app. The __name__ is a Python special
variable — Flask uses it to figure out where to look
for related files. Just type it as shown.
@app.route("/")
def home():
return "Hello, web!"
This is the route — the bridge between URL and
function. The @app.route("/") says “when someone
visits the root URL /, run this function.” The
function returns the response.
if __name__ == "__main__":
app.run(debug=True)
Run the server when this file is executed directly.
debug=True enables auto-reload (the server
restarts when you edit the file) and detailed error
pages.
Localhost — what’s 127.0.0.1?
Both 127.0.0.1 and localhost are
shorthand for “this machine.” Your server runs on
your computer, and your browser connects to your
computer.
The :5000 part is the port number — like a
specific door on your machine. Flask defaults to
5000. (HTTP uses 80, HTTPS uses 443 — those are the
“front doors” most websites use.)
For now: this is a local server. Only your machine can reach it. Real deployment (Session 12) puts it on a real public URL.
Edit and reload
Change “Hello, web!” to something else (your name, a quirky greeting, anything). Save.
Watch the terminal — you’ll see “Detected change in ‘app.py’, reloading.”
Reload the browser. The new text appears. This is the dev loop — edit Python, save, reload page, see change.
Add another route
@app.route("/about")
def about():
return "This is the about page!"
Save. The server reloads.
Visit http://127.0.0.1:5000/about — see your new
page.
You have a multi-page server. Each route function returns whatever HTML (or text) you want. Add more:
@app.route("/contact")
def contact():
return "Email me at example@example.com"
Visit /contact — there it is.
Checkpoint: Your Flask server runs locally, serves at least 2 different pages, and reloads when you save. This is the natural stop point if class is cut short.
Part B: Return real HTML
So far the responses are plain text. The browser tries to interpret them, but without HTML structure, they look raw. Time to return real HTML.
A page with HTML
@app.route("/")
def home():
return """
<!DOCTYPE html>
<html>
<head>
<title>My Flask Site</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 60px auto;
padding: 20px;
background: #f4f1eb;
}
h1 { color: #2c3e50; }
</style>
</head>
<body>
<h1>Welcome!</h1>
<p>This is my first Flask site.</p>
<ul>
<li><a href="/about">About me</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
"""
The """...""" is a Python multi-line string —
perfect for HTML.
Save. Reload. A real page with styling and links.
The issue: writing HTML inside a Python string is ugly. It’s hard to read, hard to maintain. We’ll fix this in Session 3 with templates — separate HTML files Flask reads.
For today, just see that you can return any HTML you want.
Each route, its own page
Build a small site with three pages, each its own route, each with its own HTML.
app.py:
from flask import Flask
app = Flask(__name__)
PAGE_STYLE = """
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 60px auto;
padding: 20px;
background: #f4f1eb;
}
h1 { color: #2c3e50; }
nav a { margin-right: 16px; color: #3498db; }
</style>
"""
NAV = """
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/projects">Projects</a>
</nav>
"""
@app.route("/")
def home():
return f"""
<html>
<head><title>Home</title>{PAGE_STYLE}</head>
<body>
{NAV}
<h1>Hi, I'm Alex.</h1>
<p>Welcome to my site.</p>
</body>
</html>
"""
@app.route("/about")
def about():
return f"""
<html>
<head><title>About</title>{PAGE_STYLE}</head>
<body>
{NAV}
<h1>About me</h1>
<p>I'm 12 years old. I'm learning Flask.</p>
</body>
</html>
"""
@app.route("/projects")
def projects():
return f"""
<html>
<head><title>Projects</title>{PAGE_STYLE}</head>
<body>
{NAV}
<h1>Projects</h1>
<p>I've built games, apps, and websites.</p>
</body>
</html>
"""
if __name__ == "__main__":
app.run(debug=True)
Save. Visit /, /about, /projects. Three
linked pages, all served by your Python server.
Notice the f-strings to insert the shared style and nav. Even with this, the code is repetitive — templates (Session 3) eliminate the repetition.
Stretch — return JSON
Real APIs (like the ones from Phase 7 Session 14) return JSON. So can you:
from flask import jsonify
@app.route("/api/info")
def api_info():
return jsonify({
"name": "My API",
"version": "1.0",
"items": ["thing1", "thing2", "thing3"]
})
Visit http://127.0.0.1:5000/api/info — you see
JSON in the browser.
That’s the start of building your own API.
Phase 7’s fetch could call this. You can make
your own backend now.
Stretch — random response
Make /joke return a random joke from a list:
import random
JOKES = [
"Why did the programmer quit his job? Because he didn't get arrays.",
"Why do Java developers wear glasses? Because they don't C#.",
"There are 10 types of people. Those who understand binary and those who don't.",
]
@app.route("/joke")
def joke():
return random.choice(JOKES)
Visit /joke. Reload. Different joke each time.
The server runs Python on every request. That’s the whole point of the back-end — dynamic responses.
Extension — see the request
from flask import request
@app.route("/info")
def info():
return f"You came from {request.user_agent}"
request is Flask’s object representing the
current incoming request. You can read all sorts of
stuff: who’s asking, what URL, what data they sent.
We’ll use request.form heavily in Session 5 for
forms.
Wrap-up
Before we leave, share with the room:
- For everyone — show your server. What pages did you make?
- Did the request-response cycle make sense? Browser asks, server answers.
- For kids who tried the JSON or random joke routes — that’s the back-end’s superpower: dynamic responses.
Today you learned:
- A server is a program that responds to web requests.
- Flask = Python web framework.
pip install flaskto install.@app.route("/")— bridge between URL and function.app.run(debug=True)— start the dev server with auto-reload.http://127.0.0.1:5000— local server URL.- Each route returns HTML (or text or JSON).
- Server runs Python on every request — responses can be different each time.
You’re writing the back-end now. Same Python as before, different runtime.
Next week: routes with parameters — URLs like
/user/alex where the part after /user/ becomes
input to your function.
If you missed this session
Open a terminal.
-
pip install flask. -
Create
app.pywith the basic Flask hello-world. -
Run with
python app.py. -
Visit
http://127.0.0.1:5000in a browser. -
Add 2-3 more routes with their own HTML.
About 30-45 minutes. By the end you should have a multi-page Flask server running locally.
Stretch and extension ideas
- Return JSON from a route.
- Random responses with
random.choice. - Read the request with
request.user_agent,request.headers, etc. - Custom error pages —
@app.errorhandler(404)for not-found. - Set the port —
app.run(debug=True, port=8000). - Multiple files — Flask apps can be split across modules. Out of scope for today.
- Try
flaskfrom terminal — instead ofpython app.py, setFLASK_APP=appandflask run.
What’s next
Next week: routes and URL parameters — making
URLs like /user/alex where the alex part
becomes a variable in your Python function. The
foundation for personalized pages.
Session 2: Routes and URL parameters
Session 2: Routes and URL parameters
Phase 8 — Flask · Session 2 of 13
What we’re learning today
Last week each route was a fixed URL — /about,
/contact. Today we make routes dynamic — URLs
like /user/alex where the alex part becomes a
variable in your Python function. By the end of
class you’ll have a server that personalizes pages
based on the URL — different greetings for different
names, different views for different items.
This is the foundation of every personalized web app.
You’ll need to remember from last time
- Flask basics —
Flask(__name__),@app.route("/"),app.run(debug=True). - The request-response cycle — browser asks, server answers.
- Returning HTML strings.
- F-strings —
f"Hello, {name}!".
Part A: Dynamic URLs
URL parameters with <name>
Add a route with a <...> part:
@app.route("/hello/<name>")
def hello(name):
return f"Hello, {name}!"
The angle brackets <name> capture the part of the
URL there as a variable. Whatever the user puts
in the URL becomes the name parameter of your
function.
Try it. Open app.py and add:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello! Try /hello/yourname"
@app.route("/hello/<name>")
def hello(name):
return f"Hello, {name}!"
if __name__ == "__main__":
app.run(debug=True)
Save. Run. Visit:
http://127.0.0.1:5000/hello/alex→ “Hello, alex!”http://127.0.0.1:5000/hello/sam→ “Hello, sam!”http://127.0.0.1:5000/hello/world→ “Hello, world!”
Same route, different responses. The URL is the input.
This is huge. One function handles infinite URLs — for every name, a personalized greeting.
How it works
The @app.route("/hello/<name>") line tells Flask:
- Match URLs that look like
/hello/SOMETHING. - Capture the
SOMETHINGpart as a variable calledname. - Pass it as an argument to the function.
The function name (def hello(name):) takes that
argument. Use it however you want — interpolate
into a string, query a database, look up data.
Multiple parameters
@app.route("/greet/<greeting>/<name>")
def greet(greeting, name):
return f"{greeting}, {name}!"
Visit /greet/howdy/sam → “howdy, sam!”
Visit /greet/welcome/alex → “welcome, alex!”
Each <...> becomes a function parameter, in order.
Type converters
By default, URL parameters are strings. For other types, add a converter:
@app.route("/double/<int:n>")
def double(n):
return f"{n} doubled is {n * 2}"
The <int:n> says “this part must be an integer,
and pass it as an int.”
Visit /double/5 → “5 doubled is 10”
Visit /double/abc → 404 (not a valid integer)
Built-in converters:
int— integers.float— decimal numbers.string(default) — anything except/.path— anything including/(use for file paths).
Build it — a personalized page
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return """
<html>
<body style="font-family: Arial; max-width: 600px; margin: 60px auto;">
<h1>Welcome!</h1>
<p>Try these pages:</p>
<ul>
<li><a href="/hello/alex">/hello/alex</a></li>
<li><a href="/hello/sam">/hello/sam</a></li>
<li><a href="/double/7">/double/7</a></li>
<li><a href="/double/100">/double/100</a></li>
</ul>
</body>
</html>
"""
@app.route("/hello/<name>")
def hello(name):
return f"""
<html>
<body style="font-family: Arial; max-width: 600px; margin: 60px auto;">
<h1>Hello, {name}!</h1>
<p>Welcome to the site, {name}.</p>
<p><a href="/">← Back home</a></p>
</body>
</html>
"""
@app.route("/double/<int:n>")
def double(n):
return f"""
<html>
<body style="font-family: Arial; max-width: 600px; margin: 60px auto;">
<h1>Double of {n} is {n * 2}</h1>
<p>Triple is {n * 3}, square is {n * n}</p>
<p><a href="/">← Back home</a></p>
</body>
</html>
"""
if __name__ == "__main__":
app.run(debug=True)
Save. Visit / to see the home page with links.
Click each. Personalized pages, dynamic math.
Checkpoint: Your server has at least one parameterized route. This is the natural stop point if class is cut short.
Part B: Build something with parameters
Time to use parameters for real.
Build a Pokemon viewer
A page per Pokemon. Hardcoded data for a few:
from flask import Flask
app = Flask(__name__)
POKEMON = {
"pikachu": {"type": "Electric", "level": 25, "color": "#ffeb3b"},
"charmander": {"type": "Fire", "level": 12, "color": "#ff5722"},
"bulbasaur": {"type": "Grass", "level": 14, "color": "#4caf50"},
"squirtle": {"type": "Water", "level": 10, "color": "#03a9f4"},
}
@app.route("/")
def home():
links = ""
for name in POKEMON:
links += f'<li><a href="/pokemon/{name}">{name.title()}</a></li>'
return f"""
<html>
<body style="font-family: Arial; max-width: 600px; margin: 60px auto;">
<h1>Pokemon Index</h1>
<ul>{links}</ul>
</body>
</html>
"""
@app.route("/pokemon/<name>")
def pokemon(name):
p = POKEMON.get(name.lower())
if not p:
return f"<h1>No Pokemon named {name}</h1>"
return f"""
<html>
<body style="font-family: Arial; max-width: 600px; margin: 60px auto; background: {p['color']}; padding: 20px;">
<h1>{name.title()}</h1>
<p>Type: {p['type']}</p>
<p>Level: {p['level']}</p>
<p><a href="/">← Back to index</a></p>
</body>
</html>
"""
if __name__ == "__main__":
app.run(debug=True)
Save. Visit / — see the index. Click any Pokemon.
A page styled in their color, with their data.
What’s new:
POKEMONdictionary — your “database” for now. Each key is a name; value is data.- The home page builds links by looping through the dictionary keys.
POKEMON.get(name.lower())—.get()returnsNoneif the key isn’t found (vs[name]which would crash).- Different background color per Pokemon — Python builds CSS dynamically.
This is the shape of every dynamic site. Data in a structure, route looks up by parameter, page displays.
Stretch — fallback / 404
Right now /pokemon/missingno returns “No Pokemon
named missingno” but with HTTP status 200 (success).
Better: return a real 404:
from flask import abort
@app.route("/pokemon/<name>")
def pokemon(name):
p = POKEMON.get(name.lower())
if not p:
abort(404)
return f"""..."""
abort(404) triggers a 404 response. Browsers
handle it specially.
Stretch — query parameters with request.args
Some sites use query strings like
/search?q=pokemon&color=red. Flask reads them via
request.args:
from flask import request
@app.route("/search")
def search():
q = request.args.get("q", "") # default empty if missing
return f"<h1>You searched for: {q}</h1>"
Visit /search?q=alex → “You searched for: alex”
Visit /search?q=pikachu → “You searched for:
pikachu”
Query parameters are common for filters, search
terms, and optional info. Different from URL
parameters (<name>) which are required parts of
the path.
Stretch — multiple Pokemon per type
@app.route("/type/<typename>")
def by_type(typename):
matches = [name for name, p in POKEMON.items()
if p["type"].lower() == typename.lower()]
items = "".join(f'<li><a href="/pokemon/{n}">{n}</a></li>'
for n in matches)
return f"""
<html>
<body>
<h1>{typename.title()}-type Pokemon</h1>
<ul>{items}</ul>
<p><a href="/">← Back</a></p>
</body>
</html>
"""
Visit /type/fire → list of Fire Pokemon.
Visit /type/water → list of Water.
Extension — favorite books / movies / songs
Adapt the Pokemon pattern to your favorite anything. A dictionary of items, a route per item, links from the home page. Same pattern, different domain.
Extension — calculator routes
@app.route("/add/<int:a>/<int:b>")
def add(a, b):
return f"{a} + {b} = {a + b}"
@app.route("/multiply/<int:a>/<int:b>")
def multiply(a, b):
return f"{a} × {b} = {a * b}"
Visit /add/5/3, /multiply/7/8, etc. A calculator
in URLs.
Wrap-up
Before we leave, share with the room:
- For everyone — show your routes. What URLs did you build?
- For Pokemon-builder kids — what’s your favorite Pokemon’s color?
- Did the “one function, infinite URLs” idea click?
- Anyone try
request.argsfor query strings?
Today you learned:
<name>in routes captures URL parts as parameters.- Type converters:
<int:n>,<float:x>. - Multiple parameters:
<a>/<b>. request.args.get("q")for query strings.abort(404)for “not found” responses.- One route function handles infinite URLs.
This is the foundation of every personalized site.
Wikipedia: /wiki/<article>. Twitter:
/<username>. YouTube: /watch?v=<video_id>. All
the same pattern as today.
Next week: templates — separate HTML files Flask reads, with placeholders for your data. The end of HTML-in-Python-strings.
If you missed this session
Open Thonny. You need Flask installed (Session 1).
-
Build the basic
app.pywith/,/hello/<name>, and/double/<int:n>routes. -
Test in browser with several different URLs.
-
(Stretch) Build the Pokemon viewer.
-
(Stretch) Add a
/searchroute usingrequest.args.
About 30-45 minutes. By the end you should have a server with at least one parameterized route.
Stretch and extension ideas
- Pokemon viewer with a dictionary “database.”
- Favorite-things site — books, movies, games.
- Calculator routes — add, multiply, etc.
- Query strings with
request.args. - 404 handling with
abort. - Path converters —
<path:filename>for paths with slashes. - Multiple converters in one route —
/range/<int:start>/<int:end>returns a list. url_for(...)— generate URLs from function names. Cleaner than hardcoding/hello/.... Preview Session 4.
What’s next
Next week: templates — separate HTML files
Flask reads, with placeholders. We replace the
ugly multi-line strings with clean .html files.
This is where Flask code starts to look
professional.
Session 3: Templates — Jinja2
Session 3: Templates — Jinja2
Phase 8 — Flask · Session 3 of 13
What we’re learning today
Returning HTML as Python strings is ugly. Today
you learn templates — separate .html files
Flask reads, with {{ placeholders }} for your
data and {% control %} blocks for loops and
conditionals. By the end your routes will be tiny
(“look up data, render template, send back”), and
your HTML will live in clean, syntax-highlighted
files.
This is the moment Flask code starts to look professional.
You’ll need to remember from last time
- Flask basics —
Flask(__name__),@app.route,app.run. - URL parameters —
<name>. - HTML structure — Phase 7.
- Python dictionaries — Phase 3.
Part A: Your first template
The templates/ folder
Flask expects HTML files in a folder called
templates/ next to your app.py. Make that
folder.
Your project structure:
my_app/
├── app.py
└── templates/
├── home.html
└── pokemon.html
A first template
Create templates/home.html:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<h1>{{ heading }}</h1>
<p>Welcome to my Flask site.</p>
</body>
</html>
The {{ ... }} is a template placeholder.
Flask replaces it with the value you pass in.
Now in app.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("home.html",
title="My Site",
heading="Welcome!")
if __name__ == "__main__":
app.run(debug=True)
Save both. Run. Visit http://127.0.0.1:5000.
You see:
Welcome!
Welcome to my Flask site.
The template filled in. Browser tab title: “My Site.”
What’s happening:
render_template("home.html", ...)— Flask readstemplates/home.html, replaces the{{ ... }}placeholders with the keyword arguments, and returns the result.{{ title }}in HTML becomes"My Site"(the value you passed).{{ heading }}becomes"Welcome!".
You can pass any Python value — strings, numbers, lists, dicts. The template handles it.
Why this is better
Compare:
# Without templates
@app.route("/")
def home():
return f"""
<!DOCTYPE html>
<html>
<head><title>{title}</title></head>
...
"""
vs.
# With templates
@app.route("/")
def home():
return render_template("home.html", title=title, ...)
The HTML lives in a .html file. Your editor
syntax-highlights it. Other people can edit
the design without touching your Python. Reusable.
Professional.
Loops in templates
Templates can loop with {% for %}:
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
Note the two kinds of curly braces:
{{ ... }}— output an expression.{% ... %}— control flow (loops, ifs, blocks).
Try it. In templates/home.html:
<!DOCTYPE html>
<html>
<head><title>Things I like</title></head>
<body>
<h1>{{ heading }}</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
In app.py:
@app.route("/")
def home():
return render_template("home.html",
heading="Things I like",
items=["pizza", "soccer", "reading"])
Save. Reload. A bulleted list with three items.
The for loop in the template runs through
items, generating one <li> for each.
Every {% for %} needs a matching {% endfor %}.
Same with {% if %} / {% endif %}.
Conditionals
{% if user %}
<p>Hello, {{ user }}!</p>
{% else %}
<p>Hello, stranger.</p>
{% endif %}
In app.py:
@app.route("/")
def home():
return render_template("home.html",
user="Alex")
Visit / — “Hello, Alex!” Comment out the
user="Alex" (or pass user=None) and reload —
“Hello, stranger.”
Filters — modify values in templates
The | symbol applies a filter:
{{ name|upper }} <!-- HELLO -->
{{ name|lower }} <!-- hello -->
{{ name|title }} <!-- Hello -->
{{ items|length }} <!-- count of items -->
{{ price|round(2) }} <!-- 19.99 -->
Common filters:
upper,lower,title— string case.length— count of items / characters.round(n)— number rounding.default("???")— fallback if value is missing.safe— mark HTML as safe (don’t escape).
Filters keep templates clean — no need to call Python helpers.
Checkpoint: You have a template with at least
one {{ variable }}, one {% for %} loop, and
one {% if %} conditional. This is the natural
stop point if class is cut short.
Part B: Refactor the Pokemon viewer
Take the Pokemon viewer from Session 2 and refactor it with templates.
Project structure
pokemon_app/
├── app.py
└── templates/
├── index.html
└── pokemon.html
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Pokemon Index</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 60px auto;
padding: 20px;
}
</style>
</head>
<body>
<h1>Pokemon Index</h1>
<ul>
{% for name in pokemon %}
<li><a href="/pokemon/{{ name }}">{{ name|title }}</a></li>
{% endfor %}
</ul>
</body>
</html>
templates/pokemon.html
<!DOCTYPE html>
<html>
<head>
<title>{{ name|title }}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 60px auto;
padding: 20px;
background: {{ data.color }};
}
</style>
</head>
<body>
<h1>{{ name|title }}</h1>
<p>Type: {{ data.type }}</p>
<p>Level: {{ data.level }}</p>
<p><a href="/">← Back to index</a></p>
</body>
</html>
app.py
from flask import Flask, render_template, abort
app = Flask(__name__)
POKEMON = {
"pikachu": {"type": "Electric", "level": 25, "color": "#ffeb3b"},
"charmander": {"type": "Fire", "level": 12, "color": "#ff5722"},
"bulbasaur": {"type": "Grass", "level": 14, "color": "#4caf50"},
"squirtle": {"type": "Water", "level": 10, "color": "#03a9f4"},
}
@app.route("/")
def index():
return render_template("index.html", pokemon=POKEMON)
@app.route("/pokemon/<name>")
def pokemon(name):
data = POKEMON.get(name.lower())
if not data:
abort(404)
return render_template("pokemon.html", name=name, data=data)
if __name__ == "__main__":
app.run(debug=True)
Save. Run. Visit /. Click around. Same site as
Session 2 — but now the HTML is in .html files
and the Python is tiny.
Compare line counts. The Python file is much shorter. The HTML is properly highlighted in your editor. Easier to read, easier to change.
This is the standard Flask shape. Routes stay small; templates do the rendering.
Auto-escaping (safety)
Type a name with HTML in it: visit /pokemon/<script>.
Flask auto-escapes the value. The <script>
shows as literal text, not as a tag. You’re
safe from XSS attacks (where a user injects
malicious HTML).
If you actually want HTML to render (rarely), use
|safe:
{{ user_html|safe }}
But default to not using |safe for any value
that comes from users.
Stretch — a base template
Both pages duplicate the <!DOCTYPE html>...
boilerplate. Extract a base template (we’ll go
deeper next session):
templates/base.html:
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
{% block content %}{% endblock %}
</body>
</html>
templates/index.html:
{% extends "base.html" %}
{% block content %}
<h1>Pokemon Index</h1>
<ul>
{% for name in pokemon %}
<li><a href="/pokemon/{{ name }}">{{ name|title }}</a></li>
{% endfor %}
</ul>
{% endblock %}
The child template extends the base, fills in the block. Common HTML lives in one place.
Full coverage of extends/block is Session 4.
Stretch — pass complex data
Templates can iterate over dictionaries:
return render_template("index.html",
pokemon=POKEMON.items())
{% for name, data in pokemon %}
<li>{{ name|title }} — {{ data.type }}</li>
{% endfor %}
{% for k, v in dict.items() %} — same as Python.
Stretch — Jinja math
<p>{{ 5 + 3 }}</p> <!-- 8 -->
<p>{{ data.level * 100 }}</p> <!-- 1200 -->
Templates can do arithmetic. Useful for derived values without changing your Python.
Extension — template comments
{# This is a Jinja comment — won't show in HTML #}
Different from <!-- HTML comments --> (which do
show in the page source). {# ... #} is invisible.
Wrap-up
Before we leave, share with the room:
- For everyone — show your refactored Pokemon viewer. Does it feel cleaner?
- For the kids who tried
extends/block— does template inheritance feel powerful? - Did the auto-escaping demo land? Type
<script>in a URL — it shows as text, not as code.
Today you learned:
templates/folder — Flask’s convention.render_template("name.html", var=value)— render a template with values.{{ variable }}— output a value.{% for %},{% endfor %}— loops.{% if %},{% else %},{% endif %}— conditionals.{{ value|filter }}— apply a filter (upper,length,default, etc.).- Auto-escaping — values are safe by default.
{# comment #}— Jinja comments.
Your Python files now do logic; templates do display. Real separation of concerns.
Next week: static files (CSS, images) and template inheritance — clean shared layouts across your whole site.
If you missed this session
Open Thonny.
-
Create a
templates/folder next to yourapp.py. -
Build
templates/home.htmlwith at least one{{ variable }}. -
Update
app.pyto userender_template. -
Add a
{% for %}loop and{% if %}conditional. -
(Stretch) Refactor the Pokemon viewer.
About 30-45 minutes. By the end you should have your routes returning rendered templates.
Stretch and extension ideas
- Refactor the Pokemon viewer completely.
- Base template with
extends/block. - Filters — try every common one.
- Math in templates.
- Multiple templates inheriting from the same base.
- Looping over dictionaries with
dict.items(). - Date formatting with the
format_datetimefilter (requires Flask-Babel — out of scope but worth knowing). - Jinja docs — bookmark https://jinja.palletsprojects.com/.
What’s next
Next week: static files — CSS, images, JS in
the right folder so Flask serves them — and
template inheritance — {% extends %} for
shared layouts. Your sites start looking like
real production apps.
Session 4: Static files and base templates
Session 4: Static files and base templates
Phase 8 — Flask · Session 4 of 13
What we’re learning today
Two cleanups today. First, static files — CSS,
images, JS — go in a static/ folder, served
automatically by Flask. Second, base templates
— shared HTML (header, footer, navigation) lives in
one file; other templates {% extends %} it. By
the end your Flask site has clean separation: HTML
in templates, CSS in stylesheets, layout shared via
inheritance. Production shape.
You’ll need to remember from last time
- Templates —
templates/folder,render_template,{{ var }},{% for %},{% if %}. - Phase 7 CSS — selectors, properties, the box model, Flexbox.
- HTML structure — semantic tags, navbar patterns.
Part A: Static files
The static/ folder
Just like templates/, Flask expects static files
(CSS, images, JS) in a folder called static/
next to your app.py:
my_app/
├── app.py
├── templates/
│ └── home.html
└── static/
├── style.css
└── images/
└── logo.png
Files in static/ are served at URLs starting
with /static/. So:
static/style.css→http://127.0.0.1:5000/static/style.cssstatic/images/logo.png→http://127.0.0.1:5000/static/images/logo.png
Linking from a template
You could hardcode the URL:
<link rel="stylesheet" href="/static/style.css">
But the right way uses Flask’s url_for:
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
url_for('static', filename='...') generates the
correct URL for the static file. Why use it?
- Works no matter where the app is mounted.
- Survives deployment to subpaths.
- Standard practice.
It’s longer to type but worth the habit.
Try it
Create static/style.css:
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: -apple-system, Arial, sans-serif;
max-width: 700px;
margin: 40px auto;
padding: 20px;
background: #f4f1eb;
color: #333;
line-height: 1.6;
}
h1 { color: #2c3e50; }
nav a {
margin-right: 16px;
color: #3498db;
text-decoration: none;
}
nav a:hover {
text-decoration: underline;
}
Update templates/home.html:
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<h1>{{ heading }}</h1>
<p>This page is styled.</p>
</body>
</html>
Save. Run. Reload. The page is styled.
Open DevTools → Network tab. Reload. You see two requests:
/(the HTML)/static/style.css(the CSS)
Both served by Flask. Two separate things — that’s how the web works.
Adding an image
Drop an image into static/images/logo.png (any
PNG you like).
In your template:
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
Same pattern — url_for('static', filename='path/ to/file').
What goes in static?
- CSS files.
- JavaScript files (for any client-side interactivity).
- Images, fonts, downloadable files.
Anything that’s served as-is (not generated by Python).
Checkpoint: Your Flask app serves at least
one CSS file from static/ and uses url_for to
link it. This is the natural stop point if class
is cut short.
Part B: Base templates with inheritance
The problem
Right now, every template has the full
<!DOCTYPE html>...</html> boilerplate. Add a new
page → copy the boilerplate. Change the navbar →
edit every page. Mess.
The solution — {% extends %}
Make a base template that has the shared structure. Each child template extends it and fills in blocks.
templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Site{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/projects">Projects</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2026 My Site</p>
</footer>
</body>
</html>
Notice the blocks — {% block title %} and
{% block content %}. Child templates fill these
in.
templates/home.html:
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome!</h1>
<p>This is my Flask site.</p>
<p>Navigation works because the navbar lives in <code>base.html</code>.</p>
{% endblock %}
templates/about.html:
{% extends "base.html" %}
{% block title %}About{% endblock %}
{% block content %}
<h1>About</h1>
<p>I'm learning Flask. This is page 2.</p>
{% endblock %}
Update app.py:
@app.route("/")
def home():
return render_template("home.html")
@app.route("/about")
def about():
return render_template("about.html")
@app.route("/projects")
def projects():
return render_template("projects.html") # add this template too
Save. Run. Visit each page. Same navbar, same footer, different content. Each page is just the main content — base.html provides the chrome.
If you change the navbar in base.html, every page
updates. One source of truth. Real production
practice.
How it works
{% extends "base.html" %}— child says “I’m based on this template.”{% block content %}...{% endblock %}— child replaces the block with new content.- The base’s other content (navbar, footer) stays the same.
Default content in a block:
{% block title %}My Site{% endblock %}
If a child doesn’t define {% block title %}, the
default (“My Site”) is used. Otherwise the child’s
content replaces it.
Add a third page
Make templates/projects.html:
{% extends "base.html" %}
{% block title %}Projects{% endblock %}
{% block content %}
<h1>My projects</h1>
<ul>
<li>Pong (Pygame)</li>
<li>Todo app (customtkinter)</li>
<li>This Flask site</li>
</ul>
{% endblock %}
Save. Visit /projects (after adding the route).
Same navbar, fresh content. Five lines of
unique HTML; everything else inherited.
Stretch — url_for for pages too
Hardcoded route URLs (<a href="/about">) get
fragile if you renamed routes. url_for works for
your own routes too:
<a href="{{ url_for('about') }}">About</a>
url_for('about') calls the function named
about (matching your def about():) and returns
its URL.
If you renamed def about(): to
def about_me():, you’d just update one place;
all url_for('about_me') calls update
automatically.
Use url_for for your routes too. Real production
discipline.
Stretch — multiple blocks
<!-- base.html -->
<head>
{% block extra_head %}{% endblock %}
</head>
Child can inject head content (extra CSS, scripts):
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='extra.css') }}">
{% endblock %}
Multiple blocks let children customize specific parts.
Stretch — {% include %}
For reusable chunks (a card, a sidebar) that aren’t the whole page:
{# templates/_card.html — partial #}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
</div>
{# templates/index.html #}
{% include "_card.html" %}
{% include "_card.html" %}
{% include "_card.html" %}
Underscored filenames are convention for “partials” — pieces of templates, not full pages.
Extension — block.super
Append to a parent block instead of replacing:
{% block title %}Special Page — {{ super() }}{% endblock %}
Combines parent and child content. Useful for title prefixes and accumulating CSS/JS.
Wrap-up
Before we leave, share with the room:
- For everyone — show your site. How many pages share the same base?
- Did changing one thing in
base.htmland seeing every page update feel powerful? - For the kids who used
url_forfor routes — does the indirection make sense?
Today you learned:
static/folder — Flask’s convention for CSS, JS, images.url_for('static', filename='...')— generate static URLs.{% extends "base.html" %}— child template inherits from a base.{% block name %}{% endblock %}— fillable spot in a base; child overrides.- One source of truth — base layout in one file, content per page.
{% include "..." %}— reusable partials.url_for('route_name')— generate URLs from route function names.
Your sites now have the shape of a real production site: clean separation between shared chrome and per-page content. Real Django sites, real Rails sites, real PHP sites — all work the same way.
Next week: forms — accept user input via POST requests. The first time the user changes data on the server.
If you missed this session
Open Thonny.
-
Create a
static/folder. Put a CSS file in it. -
Link from your templates with
url_for. -
Create
templates/base.htmlwith{% block %}tags. -
Refactor at least 2 pages to extend the base.
About 30-45 minutes. By the end your site should share a base layout.
Stretch and extension ideas
url_forfor routes too — clean indirection.- Multiple blocks — content, title, extra_head, etc.
{% include %}partials — reusable components.block.super()— append to parent block.- CSS variables in your stylesheet (Phase 7 callback).
- Flexbox layouts in your CSS (Phase 7 callback).
- Mobile-responsive design with
@mediaqueries. - Error templates —
templates/404.html, registered with@app.errorhandler(404). - Favicon — drop
static/favicon.icoand add<link rel="icon">in base.
What’s next
Next week: forms and POST requests. Users submit forms; your Python processes them and responds. The first time data flows from the browser to your server.
Session 5: Forms and POST requests
Session 5: Forms and POST requests
Phase 8 — Flask · Session 5 of 13
What we’re learning today
So far the user reads from your server. Today
they send data to it. Forms in HTML, processed
in Python via request.form, with the standard
GET-then-POST pattern: show the form on GET,
process it on POST, redirect to a result page. By
the end you’ll have a guestbook where any visitor
can leave a message.
This is the first time the user changes what your server holds.
You’ll need to remember from last time
- Templates with
render_template. - Base templates with
extends/block. - Phase 7 Session 7 — HTML forms (
<form>,<input>,<button>). - Lists in Python — for the guestbook.
Part A: GET vs POST
Two kinds of requests
Browsers send two main kinds of requests:
- GET — “give me this page.” The default for every link click and URL typed in the bar. No body — just URL.
- POST — “here’s some data; do something with
it.” Used by forms with
method="post". Has a body — the form data.
So far, every route you wrote handled GET. To handle POST, you tell Flask explicitly:
@app.route("/submit", methods=["GET", "POST"])
def submit():
...
By default routes only handle GET. Listing
["GET", "POST"] means “this route handles both.”
request.method — which one?
Inside the route, check which method the browser used:
from flask import request
@app.route("/submit", methods=["GET", "POST"])
def submit():
if request.method == "POST":
# Process the form data
return "Got your data!"
else:
# Show the form
return "<form method='post'><input name='x'><button>Send</button></form>"
The same URL can show a form (on GET) and process the form (on POST). Standard pattern.
request.form — read submitted data
When a form is submitted, the data is in
request.form — a dict-like object keyed by input
name attributes:
<input type="text" name="message">
text = request.form.get("message")
request.form.get("name") — same .get() pattern
as Python dicts. Returns None if missing.
request.form["name"] also works but throws an
error on missing keys.
Build it — a simple form
Project structure:
form_app/
├── app.py
├── templates/
│ ├── base.html
│ └── form.html
└── static/
└── style.css
templates/form.html:
{% extends "base.html" %}
{% block title %}Send a message{% endblock %}
{% block content %}
<h1>Send me a message</h1>
<form method="post">
<label for="name">Your name:</label>
<input type="text" id="name" name="name" required>
<label for="message">Your message:</label>
<textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Send</button>
</form>
{% endblock %}
Note method="post" on the form — this is what
makes the submit a POST.
app.py:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
name = request.form.get("name")
message = request.form.get("message")
return f"<h1>Thanks, {name}!</h1><p>You said: {message}</p>"
return render_template("form.html")
if __name__ == "__main__":
app.run(debug=True)
Add templates/base.html and static/style.css
from previous sessions.
Save. Run. Visit /. See the form. Type a name
and message. Submit. You see your data echoed
back.
You just received data from the user. First time. The web is now interactive.
The redirect-after-POST pattern
There’s a problem with the above. After submit,
the URL is still / but the page shows the
result. Refresh the page — the browser asks
“resubmit?” because it tries to repeat the POST.
Annoying.
The fix: after processing a POST, redirect to a different URL (or back to the same one, as a GET). This is the POST-Redirect-GET pattern.
Update app.py:
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
# In-memory storage — resets on server restart
messages = []
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
name = request.form.get("name")
message = request.form.get("message")
messages.append({"name": name, "message": message})
return redirect(url_for("home")) # GET back to home
return render_template("form.html", messages=messages)
Update templates/form.html to show messages:
{% extends "base.html" %}
{% block title %}Guestbook{% endblock %}
{% block content %}
<h1>Guestbook</h1>
<form method="post">
<label for="name">Your name:</label>
<input type="text" id="name" name="name" required>
<label for="message">Your message:</label>
<textarea id="message" name="message" rows="4" required></textarea>
<button type="submit">Sign the guestbook</button>
</form>
<h2>Messages</h2>
{% if messages %}
{% for m in messages %}
<div class="message">
<strong>{{ m.name }}:</strong>
<p>{{ m.message }}</p>
</div>
{% endfor %}
{% else %}
<p>No messages yet. Be the first!</p>
{% endif %}
{% endblock %}
Save. Submit a few messages. Each one appears in the list. Refresh — no resubmit warning. Real guestbook.
Pattern recap:
- GET
/→ show the form + existing messages. - POST
/→ process, store, redirect back to GET/. - The redirect = a fresh GET, no resubmit warning.
Memorize this pattern. Every form-processing route uses it.
Note on the messages list
The messages = [] lives in your Python module.
It’s stored in memory — when you stop the
server, the list is gone. Sessions 7-8 add a
database so the data really persists.
For now: in-memory is fine for learning the form pattern.
Checkpoint: Your guestbook accepts messages, stores them, and shows them on refresh without a resubmit warning. This is the natural stop point if class is cut short.
Part B: Validation and feedback
Server-side validation
Browser-side required attributes catch some
issues but can be bypassed. Server-side
validation is the safety net.
@app.route("/", methods=["GET", "POST"])
def home():
error = None
if request.method == "POST":
name = request.form.get("name", "").strip()
message = request.form.get("message", "").strip()
if not name or not message:
error = "Name and message are required."
elif len(message) > 500:
error = "Message must be under 500 characters."
else:
messages.append({"name": name, "message": message})
return redirect(url_for("home"))
return render_template("form.html", messages=messages, error=error)
Show the error in the template:
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
Add to static/style.css:
.error {
background-color: #ffe5e5;
color: #c0392b;
padding: 12px;
border-left: 4px solid #c0392b;
margin-bottom: 16px;
}
Try submitting empty (after removing required from
the input): error message appears. Try a 600-
character message: error.
Stretch — preserve form state on error
When the error shows, the form is empty again. Bad UX. Fix by passing the previous values back:
return render_template("form.html",
messages=messages,
error=error,
name=name,
message=message)
In the template:
<input type="text" id="name" name="name" value="{{ name|default('') }}" required>
<textarea id="message" name="message" rows="4" required>{{ message|default('') }}</textarea>
Now after an error, the form keeps what the user typed. Real UX practice.
Stretch — different forms, different routes
You can have separate routes for showing vs. processing:
@app.route("/submit-form")
def show_form():
return render_template("form.html")
@app.route("/submit", methods=["POST"])
def process_form():
name = request.form.get("name")
# process...
return redirect(url_for("show_form"))
The form’s action="/submit" posts to the second
route.
The single-route GET-or-POST is more common — keep both behaviors together. But knowing the multi-route variant exists is useful.
Stretch — checkboxes and selects
<input type="checkbox" name="newsletter" id="newsletter">
<label for="newsletter">Subscribe to newsletter</label>
<label for="topic">Topic:</label>
<select name="topic" id="topic">
<option value="general">General</option>
<option value="bug">Bug report</option>
</select>
newsletter = request.form.get("newsletter") # "on" if checked, None if not
topic = request.form.get("topic") # "general" or "bug"
For checkboxes, the value is "on" (or whatever
value= you set) when checked, None (missing)
when unchecked. Use if newsletter: to check.
Extension — multiple values
For multi-select or multiple checkboxes with the
same name, use request.form.getlist:
<input type="checkbox" name="hobbies" value="reading">
<input type="checkbox" name="hobbies" value="coding">
<input type="checkbox" name="hobbies" value="music">
hobbies = request.form.getlist("hobbies") # ["reading", "coding"]
Returns a list of all values for that name.
Extension — file uploads
<form method="post" enctype="multipart/form-data">
<input type="file" name="upload">
<button type="submit">Upload</button>
</form>
file = request.files.get("upload")
if file:
file.save(f"static/uploads/{file.filename}")
The enctype is required for file uploads. Beyond
that — pretty straightforward. (Real apps need to
sanitize filenames and check types.)
Wrap-up
Before we leave, share with the room:
- For everyone — show your guestbook. How many messages?
- Did the GET-then-POST-then-redirect pattern make sense?
- For the kids who added validation — what error did you trigger?
- Anyone notice messages disappear when the server restarts? That’s why we need a database (Session 7).
Today you learned:
- GET vs POST — read vs send data.
methods=["GET", "POST"]to handle both.request.methodto check which.request.form.get("name")to read form data.- POST-Redirect-GET pattern — redirect after processing to avoid resubmit warnings.
redirect(url_for("route_name"))for redirects.- Server-side validation — never trust the client.
request.form.getlistfor multi-value fields.
The first time the user changes data on your server. The web is no longer read-only.
Next week: sessions and flash messages — remember things across requests, show one-time notifications.
If you missed this session
Open Thonny.
-
Build a simple form template with name and message inputs.
-
Set up
app.pywith a single route handling both GET and POST. -
Use
request.form.get(...)to read the data. -
Implement the POST-Redirect-GET pattern with
redirect(url_for(...)). -
(Stretch) Add server-side validation.
About 45-60 minutes. By the end you should have a working guestbook.
Stretch and extension ideas
- Server-side validation (above).
- Preserve form state on error.
- Checkboxes, selects, multi-value fields.
- File uploads (advanced).
- CSRF protection with Flask-WTF (advanced — out of curriculum scope).
- Form deletion — delete-message buttons (uses
POST to
/delete/<id>). - Edit messages — edit form on GET, save on POST.
What’s next
Next week: sessions and flash messages — the server remembers visitors across requests (session cookies), and you can flash one-time messages like “Message sent!” that show on the next page load.
Session 6: Sessions and flash messages
Session 6: Sessions and flash messages
Phase 8 — Flask · Session 6 of 13
What we’re learning today
The HTTP protocol is stateless — every request is independent. The server has no idea this is the same visitor as a moment ago. Sessions fix that: Flask stores a tiny encrypted cookie in the browser that lets your code remember things across requests. Flash messages are a one-shot variant — show a notification on the next page load, then forget. By the end of class your guestbook will greet returning visitors by name and confirm their submissions.
You’ll need to remember from last time
- Forms and POST —
request.form, the POST-Redirect-GET pattern. - Templates —
render_template,{{ }},{% if %}. - Python dictionaries —
sessionworks like one.
Part A: Sessions
What’s a session?
A session is the server’s memory of who’s visiting right now. Implemented via cookies — the server gives the browser a small encrypted token, the browser sends it with every request, the server decodes it and knows “this is the same visitor.”
In Flask, the session object looks like a
dictionary but persists across requests for a
single visitor.
session["name"] = "Alex" # save
greeting = session.get("name") # load on next request
session.pop("name", None) # delete
Set up a SECRET_KEY
For Flask to sign session cookies (so they can’t be
tampered with), you need a secret key. Add to
app.py:
app = Flask(__name__)
app.secret_key = "change-me-to-anything-random"
For real apps, this should be a long random string, loaded from a config file or environment variable — not checked into Git. For class, any string works.
Try it — remember a visitor
Open Thonny. Save as app.py:
from flask import Flask, render_template, request, redirect, url_for, session
app = Flask(__name__)
app.secret_key = "dev-secret"
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
session["name"] = request.form.get("name")
return redirect(url_for("home"))
name = session.get("name")
return render_template("home.html", name=name)
templates/home.html:
<!DOCTYPE html>
<html>
<head><title>Home</title></head>
<body>
{% if name %}
<h1>Welcome back, {{ name }}!</h1>
<p><a href="/logout">Forget me</a></p>
{% else %}
<h1>Hello, stranger.</h1>
<form method="post">
<label>Your name: <input type="text" name="name" required></label>
<button type="submit">Remember me</button>
</form>
{% endif %}
</body>
</html>
Add a logout route:
@app.route("/logout")
def logout():
session.pop("name", None)
return redirect(url_for("home"))
Save. Run. Visit /. You’re greeted as a
stranger. Type your name. Submit.
Now reload the page. You’re greeted by name. Close the tab. Reopen. Still greeted by name (within the session lifetime).
That’s a session. The server remembers you between requests.
Click “Forget me” → session cleared → back to “stranger.”
Inspect the cookie
Open DevTools → Application → Cookies. You see a
session cookie with an encoded value. Click it
— the value is gibberish. That’s the encrypted
session.
If someone tampered with this value, Flask would reject it (because the signature wouldn’t match). That’s why the SECRET_KEY matters.
Sessions are per browser
Sessions are stored as cookies, so each browser has its own session. Open a different browser (or incognito) — you’re a fresh visitor. The session isn’t user accounts (those come in Session 9); it’s just “this specific browser.”
Checkpoint: Your app remembers a visitor’s name across page loads via the session. This is the natural stop point if class is cut short.
Part B: Flash messages
The need
After a form POST, you redirect (Session 5 pattern).
But how do you tell the user “Got it!” on the next
page? You can’t put it in the URL (ugly). You can’t
put it in request (that’s the next request).
You can use the session — but you’d have to clear
it manually.
Flash messages are the solution: store a one-shot message, displayed on the next request, auto-cleared after.
flash and get_flashed_messages
from flask import flash
@app.route("/submit", methods=["POST"])
def submit():
# ... process ...
flash("Saved!")
return redirect(url_for("home"))
In any template (usually base.html):
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class="flashes">
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
The {% with %} block creates a temporary variable
scoped to the block. get_flashed_messages()
returns the list of pending messages and removes
them — they only show once.
Add to the guestbook
Take the guestbook from Session 5. Add flash:
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
name = request.form.get("name", "").strip()
message = request.form.get("message", "").strip()
if not name or not message:
flash("Both fields required.", "error")
else:
messages.append({"name": name, "message": message})
flash("Message posted!", "success")
return redirect(url_for("home"))
return render_template("guestbook.html", messages=messages)
The second arg to flash is a category — useful
for styling:
flash("Saved!", "success")
flash("Wrong password.", "error")
flash("Heads up.", "warning")
In the template, get them with categories:
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flashes">
{% for category, msg in messages %}
<li class="flash flash-{{ category }}">{{ msg }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
Style with CSS:
.flashes { list-style: none; padding: 0; }
.flash { padding: 12px; margin-bottom: 8px; border-radius: 4px; }
.flash-success { background: #d4edda; color: #155724; }
.flash-error { background: #f8d7da; color: #721c24; }
.flash-warning { background: #fff3cd; color: #856404; }
Save. Submit a form. A green “Message posted!” appears. Submit empty. A red error appears. Reload. Messages gone (one-shot).
Where to put the flash display
Best place: base.html — every page shows
flashes. Then any route can flash(...) and it
shows on the next page load.
Update templates/base.html:
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class="flashes">
{% for category, msg in messages %}
<li class="flash flash-{{ category }}">{{ msg }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
Now any flash anywhere in your app shows up on the next render. Beautiful.
Stretch — “remember me” checkbox
Some sites have “remember me” — sessions persist beyond browser close.
Flask sessions default to session lifetime (when the browser closes). Make them permanent:
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
Now sessions last 30 days even if the browser closes.
Stretch — multiple flashes per request
Multiple flashes accumulate:
flash("Welcome back!", "success")
flash("Don't forget to verify your email.", "warning")
return redirect(url_for("home"))
Both show on the next page. Useful for “saved AND here’s a note” scenarios.
Stretch — view counter using session
@app.route("/")
def home():
visits = session.get("visits", 0)
session["visits"] = visits + 1
return f"You've visited {session['visits']} times."
Each visit, the counter goes up. Stored per-browser. Real personalization.
Extension — store more than strings
Sessions can hold any JSON-serializable data:
session["cart"] = ["pizza", "soda", "chips"]
session["user"] = {"name": "Alex", "level": 5}
Lists, dicts, all fine. (Same data types as JSON.)
Wrap-up
Before we leave, share with the room:
- For everyone — show the “remember me” demo. Did closing/reopening keep your name?
- Did seeing the session cookie in DevTools click? That’s the actual storage.
- For the kids who added flash messages — show one popping up.
- Anyone notice the difference between session and flash?
Today you learned:
- HTTP is stateless — every request is independent.
- Sessions = server-signed cookies storing per-visitor state.
session["key"] = value— store.session.get("key")— read.session.pop("key", None)— delete.app.secret_keyrequired for sessions.- Flash messages = one-shot per-visit notifications.
flash("..."),flash("...", "category").get_flashed_messages(with_categories=true).- Put flash display in base.html — works everywhere.
This is the foundation of personalization and user feedback in any web app. Sessions remember who; flashes confirm what.
Next week: SQLite — your first real database. Data that survives server restarts.
If you missed this session
Open Thonny.
-
Add
app.secret_key = "..."toapp.py. -
Use
session["name"] = ...andsession.get("name")to remember a visitor’s name. -
Add a logout route that clears the session.
-
(Stretch) Add flash messages to your form.
About 30-45 minutes. By the end your app should remember visitors by name and show flash messages.
Stretch and extension ideas
- Permanent sessions with custom lifetime.
- Multiple flashes per request.
- Categories with custom CSS.
- Visit counter via session.
- Store complex data (lists, dicts) in session.
- Session-based shopping cart — add items, view cart, clear.
- Theme preference stored in session.
What’s next
Next week: SQLite — your first real database.
Move beyond in-memory messages = [] and store
data that survives server restarts. Just Python
- SQL, no Flask yet. Then Session 8 ties them together.
Session 7: SQLite — your first database
Session 7: SQLite — your first database
Phase 8 — Flask · Session 7 of 13
What we’re learning today
In Phase 5 you saved data to JSON files. In Phase 7 you used localStorage. Both work for one user, one machine, simple cases. Real apps use a database — structured storage with powerful queries, multi-user, designed for it. Today you meet SQLite — Python’s built-in database — and the SQL language for talking to it. By the end you’ll have a database file, a table, and you’ll be running queries against it.
This is a plain Python session. No Flask. We focus on SQL first, then tie it to Flask in Session 8.
You’ll need to remember from last time
- Python lists and dictionaries.
with open(...) as f:pattern — similar shape forwith sqlite3.connect(...).for row in result:— iterating.- Phase 3 Session 11 (file I/O) — same need for persistence, much less powerful tool.
Part A: What’s a database?
The model
A database is structured storage for data. Think of it like a spreadsheet with strict rules:
- Tables (like worksheets) — each table holds
one kind of thing. A
userstable, apoststable. - Rows (like spreadsheet rows) — each row is one item in the table. One user. One post.
- Columns (like spreadsheet columns) — each column is a property of the items. Name, age, email.
- Schema — the rules: which tables exist, what columns they have, what types those columns hold.
Example users table:
| id | name | age |
|---|---|---|
| 1 | Alex | 12 |
| 2 | Sam | 10 |
| 3 | Pat | 14 |
Three rows, three columns.
Why a database?
vs. JSON file:
- Queries — “give me users older than 11” takes one line; with JSON you’d loop and filter.
- Speed — databases are fast even with thousands of rows.
- Multi-user — multiple Flask requests can read/write at once safely.
- Integrity — schemas reject bad data.
- Real apps use them.
vs. localStorage:
- localStorage is per-browser. Database is for everyone.
What’s SQLite?
SQLite is a database that:
- Stores everything in a single file (no separate server program).
- Built into Python — no install needed (it’s
the
sqlite3module). - Used by millions of apps — Firefox, Chrome, iOS, Android, countless tools.
- Perfect for small-to-medium apps.
Other databases (PostgreSQL, MySQL) are more powerful for huge apps but require running a separate server. SQLite is a great starting point.
What’s SQL?
SQL (Structured Query Language) is the language for talking to databases. Mostly the same across all SQL databases (PostgreSQL, MySQL, SQLite, etc.).
Four main commands:
SELECT— read data.INSERT— add new row.UPDATE— change existing row.DELETE— remove row.
Plus:
CREATE TABLE— define a table.
That’s most of what you need.
Try it — Python REPL
Open Thonny. Open the Shell (View → Shell, or just type at the bottom). We’ll work in Python’s interactive mode.
>>> import sqlite3
>>> conn = sqlite3.connect("test.db")
>>> cursor = conn.cursor()
Three lines. Walk through:
import sqlite3— Python’s built-in module.sqlite3.connect("test.db")— open (or create) a database file. After this line, a file calledtest.dbexists in your folder.conn.cursor()— get a cursor for running SQL.
Create a table
>>> cursor.execute("""
... CREATE TABLE pets (
... id INTEGER PRIMARY KEY,
... name TEXT NOT NULL,
... species TEXT NOT NULL,
... age INTEGER
... )
... """)
The triple-quoted string is multi-line SQL. Walk through:
CREATE TABLE pets— make a table namedpets.id INTEGER PRIMARY KEY— id is an integer, automatically unique and auto-incrementing.name TEXT NOT NULL— name is text, required.species TEXT NOT NULL— species is text, required.age INTEGER— age is an integer (optional — noNOT NULL).
After this, the table exists. Try the line again — you’ll get an error: “table pets already exists.” Tables are persistent.
Insert rows
>>> cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Whiskers', 'cat', 3)")
>>> cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Rex', 'dog', 5)")
>>> cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Buddy', 'dog', 2)")
>>> conn.commit()
Walk through:
INSERT INTO pets (name, species, age) VALUES (...)— add a new row.conn.commit()— save the changes to disk. Without it, your inserts vanish if the program ends.
commit() is like saving a file. Required after
any change.
Read rows
>>> cursor.execute("SELECT * FROM pets")
>>> for row in cursor.fetchall():
... print(row)
You see:
(1, 'Whiskers', 'cat', 3)
(2, 'Rex', 'dog', 5)
(3, 'Buddy', 'dog', 2)
Each row comes back as a tuple — the columns in order.
The * in SELECT * means “all columns.” Or
specify:
>>> cursor.execute("SELECT name, age FROM pets")
>>> for row in cursor.fetchall():
... print(row)
(Whiskers, 3)
(Rex, 5)
(Buddy, 2)
Filter with WHERE
>>> cursor.execute("SELECT name FROM pets WHERE species = 'dog'")
>>> for row in cursor.fetchall():
... print(row)
(Rex,)
(Buddy,)
The WHERE clause filters rows.
>>> cursor.execute("SELECT name FROM pets WHERE age > 2")
>>> for row in cursor.fetchall():
... print(row)
(Whiskers,)
(Rex,)
Operators: =, <, >, <=, >=, != (or
<>), LIKE 'pattern%' (substring match).
Update and delete
>>> cursor.execute("UPDATE pets SET age = 4 WHERE name = 'Whiskers'")
>>> conn.commit()
>>> cursor.execute("DELETE FROM pets WHERE name = 'Buddy'")
>>> conn.commit()
UPDATE changes rows. DELETE removes rows. Both
need conn.commit().
WARNING: A DELETE or UPDATE without
WHERE affects every row. DELETE FROM pets
empties the table. Always include WHERE for
targeted changes.
Close the connection
>>> conn.close()
Always close when done. (Real apps do this in Flask via context managers — Session 8.)
Checkpoint: You created a table, inserted at least 2 rows, queried with SELECT and WHERE, updated and deleted at least once. This is the natural stop point if class is cut short.
Part B: A complete Python script
Time to put it all in a runnable file (not REPL).
Build it
Save as pets.py:
import sqlite3
conn = sqlite3.connect("pets.db")
cursor = conn.cursor()
# Create the table (if it doesn't exist)
cursor.execute("""
CREATE TABLE IF NOT EXISTS pets (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
species TEXT NOT NULL,
age INTEGER
)
""")
# Add some pets (only if the table is empty)
cursor.execute("SELECT COUNT(*) FROM pets")
count = cursor.fetchone()[0]
if count == 0:
cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Whiskers', 'cat', 3)")
cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Rex', 'dog', 5)")
cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Tweety', 'bird', 1)")
cursor.execute("INSERT INTO pets (name, species, age) VALUES ('Goldie', 'fish', 2)")
conn.commit()
print(f"Added 4 pets.")
else:
print(f"Already have {count} pets.")
# List all pets
print("\nAll pets:")
for row in cursor.execute("SELECT * FROM pets"):
print(f" {row[1]} ({row[2]}, age {row[3]})")
# Filter
print("\nDogs only:")
for row in cursor.execute("SELECT name, age FROM pets WHERE species = 'dog'"):
print(f" {row[0]} (age {row[1]})")
conn.close()
Save. Run.
Added 4 pets.
All pets:
Whiskers (cat, age 3)
Rex (dog, age 5)
Tweety (bird, age 1)
Goldie (fish, age 2)
Dogs only:
Rex (age 5)
Run again. The first message changes to “Already have 4 pets” because the table now has data. Survives between runs — that’s the database.
What’s new:
CREATE TABLE IF NOT EXISTS— only create if missing. Safe to run repeatedly.SELECT COUNT(*)— count rows. Returns one row with one column.cursor.fetchone()[0]— get the first (only) row, then the first (only) column.for row in cursor.execute(...)— shortcut: execute and iterate in one step.
Stretch — parameterized queries (SQL injection)
Never string-format user input into SQL:
# DON'T DO THIS
name = "user input"
cursor.execute(f"SELECT * FROM pets WHERE name = '{name}'")
If name is '; DROP TABLE pets; --, your
table is destroyed. Real attack — happens
constantly to bad code.
The right way: parameterized queries. Use ?
placeholders:
name = "Whiskers"
cursor.execute("SELECT * FROM pets WHERE name = ?", (name,))
The (name,) is a tuple with one element. The
database engine substitutes safely — no injection
possible.
For multiple parameters:
cursor.execute(
"INSERT INTO pets (name, species, age) VALUES (?, ?, ?)",
("Polly", "parrot", 7)
)
Always use parameterized queries when data comes from outside (forms, URLs, files). We’ll hammer this in Session 8.
Stretch — DB Browser for SQLite
A free tool for viewing SQLite databases:
sqlitebrowser.org.
Download, install, open pets.db. Visual table
view. Run queries in a UI.
Useful for:
- Inspecting your data.
- Trying queries before putting them in code.
- Editing data without writing code.
For class: optional but helpful.
Stretch — multiple tables and JOINs
cursor.execute("""
CREATE TABLE owners (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
""")
cursor.execute("""
ALTER TABLE pets ADD COLUMN owner_id INTEGER
""")
Then a JOIN query:
SELECT pets.name, owners.name
FROM pets
JOIN owners ON pets.owner_id = owners.id
JOINs let you connect data across tables. Out of session scope but worth knowing about.
Extension — explore SQL
SQLBolt — interactive SQL tutorial. SQL Murder Mystery — learn SQL by solving a murder. Genuinely fun.
Wrap-up
Before we leave, share with the room:
- For everyone — show your
pets.pyrunning. Did the data persist between runs? - Did SQL feel different from Python? Same logic, different shape.
- For the kids who tried parameterized queries — did the SQL injection demo land?
- Anyone install DB Browser? Show your data.
Today you learned:
- A database is structured storage with powerful queries.
- SQLite is built into Python; data lives in a single file.
- SQL is the language for talking to databases.
sqlite3.connect(...)+cursor()to get started.CREATE TABLEto define structure.INSERTto add rows.SELECTto read;WHEREto filter.UPDATEto change;DELETEto remove.conn.commit()to save changes.- Parameterized queries (
?) to prevent SQL injection — required for any external data.
Next week: tie SQLite to Flask. Forms write to the database; templates display query results. Real persistent web apps.
If you missed this session
Open Thonny.
-
Run a few
sqlite3commands in the Python shell to create a table and insert rows. -
Build the
pets.pyscript from Part B. -
Run it twice — verify data persists.
-
(Stretch) Try parameterized queries.
About 45-60 minutes. By the end you should be
comfortable with basic SQL and the sqlite3
module.
Stretch and extension ideas
- Parameterized queries — required practice.
- DB Browser for SQLite for visual inspection.
- More SQL —
ORDER BY,LIMIT,LIKE,IN,BETWEEN. - Aggregate functions —
COUNT,SUM,AVG,MAX,MIN. - Multiple tables with foreign keys + JOINs.
- Transactions —
BEGIN,COMMIT,ROLLBACK. - Indexes for query speed.
- SQLBolt or SQL Murder Mystery — interactive practice.
What’s next
Next week: SQLite in Flask — every CRUD operation tied to a route. The guestbook from Session 5 gets a real database — messages survive restarts. You’re now building real persistent web apps.
Session 8: Database in Flask — CRUD operations
Session 8: Database in Flask — CRUD operations
Phase 8 — Flask · Session 8 of 13
What we’re learning today
You know Flask. You know SQLite. Today they combine. Routes read from the database, forms write to it. By the end your guestbook from Session 5 will store messages in a real database — they survive server restarts. You’ll also learn the four CRUD operations (Create, Read, Update, Delete) tied to URL routes — the shape of every real web app.
You’ll need to remember from last time
- SQLite basics —
connect,cursor,execute,commit. - SQL — CREATE, INSERT, SELECT, UPDATE, DELETE.
- Parameterized queries —
?placeholders. - Flask — routes, templates, forms, POST-Redirect-GET.
Part A: Connecting Flask to SQLite
A helper function
Talking to the database from every route gets repetitive. Let’s make a helper:
import sqlite3
def get_db():
conn = sqlite3.connect("guestbook.db")
conn.row_factory = sqlite3.Row # rows behave like dicts
return conn
Two things new:
conn.row_factory = sqlite3.Row— instead of plain tuples, rows behave like dicts. Sorow["name"]works, not justrow[0]. Way more readable.- The function keeps connection setup in one place.
Now any route can call get_db() to get a working
connection.
Create the table on startup
Run the CREATE TABLE once when the app starts.
Add to app.py:
def init_db():
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
init_db() # runs when the app starts
The created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
column auto-fills with the current time when a row
is inserted. Free metadata.
Refactor the guestbook
Take the Session 5 guestbook. Replace the in-memory
messages = [] with database calls.
Full app.py:
from flask import Flask, render_template, request, redirect, url_for, flash
import sqlite3
app = Flask(__name__)
app.secret_key = "dev-secret"
def get_db():
conn = sqlite3.connect("guestbook.db")
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
init_db()
@app.route("/", methods=["GET", "POST"])
def home():
conn = get_db()
if request.method == "POST":
name = request.form.get("name", "").strip()
message = request.form.get("message", "").strip()
if not name or not message:
flash("Both fields required.", "error")
else:
conn.execute(
"INSERT INTO messages (name, message) VALUES (?, ?)",
(name, message)
)
conn.commit()
flash("Message posted!", "success")
conn.close()
return redirect(url_for("home"))
rows = conn.execute(
"SELECT * FROM messages ORDER BY created_at DESC"
).fetchall()
conn.close()
return render_template("guestbook.html", messages=rows)
if __name__ == "__main__":
app.run(debug=True)
Update templates/guestbook.html:
{% extends "base.html" %}
{% block title %}Guestbook{% endblock %}
{% block content %}
<h1>Guestbook</h1>
<form method="post">
<label>Your name: <input type="text" name="name" required></label>
<label>Message: <textarea name="message" rows="3" required></textarea></label>
<button type="submit">Post</button>
</form>
<h2>Messages</h2>
{% if messages %}
{% for m in messages %}
<article class="message">
<header><strong>{{ m.name }}</strong> · <time>{{ m.created_at }}</time></header>
<p>{{ m.message }}</p>
</article>
{% endfor %}
{% else %}
<p>No messages yet.</p>
{% endif %}
{% endblock %}
Save. Run. Submit a few messages. They appear, in newest-first order, with timestamps. Stop the server. Start it again. Messages still there.
The database is doing its job.
What changed from Session 5
| Session 5 | Session 8 |
|---|---|
messages = [] (in-memory) | messages table in SQLite |
messages.append(...) | INSERT INTO messages |
for m in messages: | SELECT * FROM messages |
| Lost on restart | Persists |
| One server only | Shared across processes |
Same UX, infinitely better backend.
Checkpoint: Your guestbook stores messages in SQLite — they survive server restarts. This is the natural stop point if class is cut short.
Part B: Full CRUD
The guestbook only does Create (insert) and Read (select). Real apps also need Update (edit) and Delete. That’s CRUD.
Add a delete button
Update templates/guestbook.html — each message
gets a delete button:
{% for m in messages %}
<article class="message">
<header><strong>{{ m.name }}</strong> · <time>{{ m.created_at }}</time></header>
<p>{{ m.message }}</p>
<form method="post" action="{{ url_for('delete', message_id=m.id) }}" style="display: inline;">
<button type="submit" class="delete-btn">Delete</button>
</form>
</article>
{% endfor %}
Add the route in app.py:
@app.route("/delete/<int:message_id>", methods=["POST"])
def delete(message_id):
conn = get_db()
conn.execute("DELETE FROM messages WHERE id = ?", (message_id,))
conn.commit()
conn.close()
flash("Message deleted.", "success")
return redirect(url_for("home"))
Save. Reload. Each message has a delete button. Click → message gone, flash confirms.
What’s new:
<int:message_id>— URL parameter (Session 2). The integer ID of the message to delete.- Form with action, not just a link, because delete is a change (POST) not a read (GET).
- Parameterized DELETE —
?for the ID.
Edit a message
Two routes:
- GET
/edit/<id>— show the edit form. - POST
/edit/<id>— save the changes.
@app.route("/edit/<int:message_id>", methods=["GET", "POST"])
def edit(message_id):
conn = get_db()
if request.method == "POST":
name = request.form.get("name", "").strip()
message = request.form.get("message", "").strip()
if not name or not message:
flash("Both fields required.", "error")
conn.close()
return redirect(url_for("edit", message_id=message_id))
conn.execute(
"UPDATE messages SET name = ?, message = ? WHERE id = ?",
(name, message, message_id)
)
conn.commit()
conn.close()
flash("Message updated.", "success")
return redirect(url_for("home"))
row = conn.execute(
"SELECT * FROM messages WHERE id = ?",
(message_id,)
).fetchone()
conn.close()
if not row:
flash("Message not found.", "error")
return redirect(url_for("home"))
return render_template("edit.html", message=row)
templates/edit.html:
{% extends "base.html" %}
{% block title %}Edit message{% endblock %}
{% block content %}
<h1>Edit message</h1>
<form method="post">
<label>Name: <input type="text" name="name" value="{{ message.name }}" required></label>
<label>Message: <textarea name="message" rows="3" required>{{ message.message }}</textarea></label>
<button type="submit">Save</button>
<a href="{{ url_for('home') }}">Cancel</a>
</form>
{% endblock %}
Update the guestbook to add an edit link per message:
<a href="{{ url_for('edit', message_id=m.id) }}">Edit</a>
Save. Click “Edit” on any message → edit form loaded with current values. Change. Submit. Message updated.
You now have full CRUD: Create, Read, Update, Delete. The shape of every CMS, every blog, every social network.
Stretch — sort and filter
Add a search:
search = request.args.get("q", "").strip()
if search:
rows = conn.execute(
"SELECT * FROM messages WHERE message LIKE ? ORDER BY created_at DESC",
(f"%{search}%",)
).fetchall()
else:
rows = conn.execute(
"SELECT * FROM messages ORDER BY created_at DESC"
).fetchall()
Add a search box in the template:
<form method="get" action="{{ url_for('home') }}">
<input type="text" name="q" value="{{ request.args.get('q', '') }}" placeholder="Search">
<button type="submit">Search</button>
</form>
The LIKE operator with % wildcards = substring
match. %pizza% matches anywhere “pizza” appears.
Visit /?q=pizza — only matching messages show.
Stretch — counts
total = conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
Show “{{ total }} messages” in the template.
Stretch — pagination
For long lists:
PAGE_SIZE = 10
page = int(request.args.get("page", 1))
offset = (page - 1) * PAGE_SIZE
rows = conn.execute(
"SELECT * FROM messages ORDER BY created_at DESC LIMIT ? OFFSET ?",
(PAGE_SIZE, offset)
).fetchall()
Add “Next page” / “Previous page” links.
LIMIT N OFFSET M = “give me N rows, skipping the
first M.”
Extension — confirm dialog
Use Phase 7 JS for delete confirmation:
<form ...
onsubmit="return confirm('Are you sure?');">
return confirm(...) cancels the submit if user
clicks Cancel.
Extension — schema migrations
When you add a column to an existing table, you need:
ALTER TABLE messages ADD COLUMN topic TEXT
Or for class: delete guestbook.db and let
init_db() recreate. (For real production: real
migration tools.)
Wrap-up
Before we leave, share with the room:
- For everyone — show your persistent guestbook. Did messages survive a restart?
- For the kids who added edit/delete — full CRUD!
- For the kids who added search — useful?
- Did the database feel powerful compared to
in-memory
messages = []?
Today you learned:
- Tie Flask routes to database operations.
get_db()helper for connection setup.row_factory = sqlite3.Rowfor dict-like rows.init_db()on startup to ensure schema.INSERTin routes for creating data.SELECTin routes for reading.UPDATEfor editing.DELETEfor removing.- The four CRUD operations mapped to routes.
<int:id>URL params for targeting specific records.- POST for changes, GET for reads.
You can now build real persistent web apps. The guestbook is the simplest version, but the shape is universal.
Next week: user accounts — signup, login, password hashing. Multiple users, each with their own data. The web’s most-used pattern.
If you missed this session
Open Thonny.
-
Take the Session 5 guestbook.
-
Add
get_db()andinit_db(). -
Replace
messages = []with SQL operations. -
Run, post messages, restart, verify they persist.
-
(Stretch) Add edit and delete routes.
About 60-90 minutes — this is a substantial session.
Stretch and extension ideas
- Edit and delete routes (above).
- Search with LIKE.
- Counts with COUNT(*).
- Pagination with LIMIT/OFFSET.
- Sort options — by name, by date, by length.
- Confirm dialog with JS.
- Mark messages as “favorite” — extra column, toggle button.
- Different SQL dialects — read about how PostgreSQL or MySQL differ. (Mostly compatible with SQLite.)
What’s next
Next week: user accounts. A users table
with hashed passwords. Signup, login, logout. The
session stores who’s logged in. Each user has
their own data (next session — the notes app).
Session 9: User accounts — signup, login, logout
Session 9: User accounts — signup, login, logout
Phase 8 — Flask · Session 9 of 13
What we’re learning today
You have a database. You have sessions. Today they combine into user accounts — signup, login, logout, and the holy “I have my own account on a thing I built” moment. You’ll also learn the critical security rule: never store plaintext passwords. By the end of class your app will let multiple people sign up, log in, and stay logged in across visits.
You’ll need to remember from last time
- Sessions —
session["key"] = value(Session 6). - SQLite + Flask —
get_db,init_db, parameterized queries (Session 8). - Forms — POST with validation (Session 5).
- Flash messages — for “logged in” / “wrong password” feedback (Session 6).
Part A: Password hashing — the critical rule
Never store plaintext passwords
The single most important security rule of web development:
Never store passwords as plain text.
If your database is ever leaked (data breach, hacker, careless backup), every password is exposed — and many users reuse passwords across sites. Your breach becomes their bank’s breach.
The fix: store a hash of the password, not the password itself.
What’s a hash?
A hash function takes input → outputs a fixed-length scrambled string. Two key properties:
- One-way. You can’t compute the input from the hash.
- Deterministic. Same input → same hash, every time.
So when a user signs up, you hash their password and store the hash. When they log in, you hash what they typed and compare to the stored hash — match means correct password.
You never need to know the original password. And neither does an attacker who steals the database.
Use werkzeug.security
Flask ships with werkzeug (a related library)
that has secure password helpers. Use them.
from werkzeug.security import generate_password_hash, check_password_hash
# When signing up:
password_hash = generate_password_hash("my_password")
# Store password_hash in the database, NOT "my_password"
# When logging in:
if check_password_hash(stored_hash, "what_user_typed"):
# correct password
else:
# wrong password
generate_password_hash produces a long string
like:
scrypt:32768:8:1$abc...
Includes the algorithm, salt, and hash. Self- contained. Just store it.
check_password_hash parses it, hashes the input
the same way, compares, returns True/False.
Don’t roll your own hashing. Use these functions. Production-grade security in two lines.
Try it
Open the Python shell:
>>> from werkzeug.security import generate_password_hash, check_password_hash
>>> h = generate_password_hash("hello")
>>> h
'scrypt:32768:8:1$...long string...'
>>> check_password_hash(h, "hello")
True
>>> check_password_hash(h, "Hello")
False
>>> check_password_hash(h, "")
False
The hash changes every time (because of random
salt), but check_password_hash matches correctly
each time:
>>> generate_password_hash("hello") == generate_password_hash("hello")
False
>>> # Different hashes — but both match "hello"
This is how passwords work everywhere on the internet.
Checkpoint: You understand why we hash passwords and how to use the two werkzeug functions. This is the natural stop point if class is cut short.
Part B: Build the auth flow
Time to build it. We’ll add users to a Flask app
with signup, login, and logout.
Project structure
auth_app/
├── app.py
├── auth.db
├── templates/
│ ├── base.html
│ ├── home.html
│ ├── signup.html
│ └── login.html
└── static/
└── style.css
app.py — full code
from flask import Flask, render_template, request, redirect, url_for, session, flash
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
app = Flask(__name__)
app.secret_key = "dev-secret"
def get_db():
conn = sqlite3.connect("auth.db")
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
conn.close()
init_db()
def current_user():
"""Returns the logged-in user row, or None."""
user_id = session.get("user_id")
if not user_id:
return None
conn = get_db()
user = conn.execute(
"SELECT * FROM users WHERE id = ?", (user_id,)
).fetchone()
conn.close()
return user
@app.context_processor
def inject_user():
"""Make `current_user` available in all templates."""
return {"current_user": current_user()}
@app.route("/")
def home():
return render_template("home.html")
@app.route("/signup", methods=["GET", "POST"])
def signup():
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
if not username or not password:
flash("Both fields required.", "error")
elif len(password) < 4:
flash("Password must be at least 4 characters.", "error")
else:
conn = get_db()
try:
conn.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, generate_password_hash(password))
)
conn.commit()
conn.close()
flash(f"Account created! Please log in.", "success")
return redirect(url_for("login"))
except sqlite3.IntegrityError:
conn.close()
flash("Username already taken.", "error")
return render_template("signup.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
conn = get_db()
user = conn.execute(
"SELECT * FROM users WHERE username = ?", (username,)
).fetchone()
conn.close()
if user and check_password_hash(user["password_hash"], password):
session["user_id"] = user["id"]
flash(f"Welcome back, {user['username']}!", "success")
return redirect(url_for("home"))
else:
flash("Invalid username or password.", "error")
return render_template("login.html")
@app.route("/logout")
def logout():
session.pop("user_id", None)
flash("Logged out.", "success")
return redirect(url_for("home"))
if __name__ == "__main__":
app.run(debug=True)
That’s the full auth system. Walk through key pieces:
UNIQUEon theusernamecolumn — the database refuses duplicates. We catchsqlite3.IntegrityErrorto tell the user.current_user()helper — readssession["user_id"], looks up the user, returns the row (or None if not logged in).@app.context_processor— makescurrent_userautomatically available in every template. No need to pass it from each route.- Login flow — find user, check_password_hash, set session.
- Logout — clear the session key.
Templates
templates/base.html:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="{{ url_for('home') }}">Home</a>
{% if current_user %}
<span>Logged in as <strong>{{ current_user.username }}</strong></span>
<a href="{{ url_for('logout') }}">Log out</a>
{% else %}
<a href="{{ url_for('login') }}">Log in</a>
<a href="{{ url_for('signup') }}">Sign up</a>
{% endif %}
</nav>
</header>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, msg in messages %}
<div class="flash flash-{{ category }}">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</body>
</html>
The nav switches between “log in / sign up” and
“logged in as X / log out” based on
current_user. The base template handles auth
display universally.
templates/home.html:
{% extends "base.html" %}
{% block content %}
{% if current_user %}
<h1>Welcome, {{ current_user.username }}!</h1>
<p>You are logged in.</p>
{% else %}
<h1>Welcome!</h1>
<p>Please <a href="{{ url_for('login') }}">log in</a>
or <a href="{{ url_for('signup') }}">sign up</a>.</p>
{% endif %}
{% endblock %}
templates/signup.html:
{% extends "base.html" %}
{% block title %}Sign up{% endblock %}
{% block content %}
<h1>Sign up</h1>
<form method="post">
<label>Username: <input type="text" name="username" required></label>
<label>Password: <input type="password" name="password" required></label>
<button type="submit">Create account</button>
</form>
{% endblock %}
templates/login.html:
{% extends "base.html" %}
{% block title %}Log in{% endblock %}
{% block content %}
<h1>Log in</h1>
<form method="post">
<label>Username: <input type="text" name="username" required></label>
<label>Password: <input type="password" name="password" required></label>
<button type="submit">Log in</button>
</form>
{% endblock %}
Note <input type="password"> — browsers show
dots instead of letters.
Save everything. Run.
Test the flow
- Visit
/. “Welcome! Please log in or sign up.” - Click sign up. Pick a username and password.
- Submit. “Account created! Please log in.”
- Click log in. Use the same credentials.
- Submit. “Welcome back, alex!” — and the nav shows “Logged in as alex” with a Log out button.
- Visit
/. “Welcome, alex!” - Log out. Back to “stranger” state.
- Try to log in with the wrong password. “Invalid username or password.”
- Try to sign up with the same username again. “Username already taken.”
You have a working auth system. Real signup, real login, real password hashing.
Inspect the database
Open auth.db in DB Browser for SQLite (Session 7
stretch). Look at the users table. The
password_hash column is gibberish — long encoded
strings. Even if someone steals the database, the
passwords are protected.
Stretch — protect routes (require login)
Often you want certain routes to require login. Pattern:
@app.route("/secret")
def secret():
if not current_user():
flash("Please log in.", "error")
return redirect(url_for("login"))
return "Top secret content!"
A decorator makes this cleaner:
from functools import wraps
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user():
flash("Please log in.", "error")
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorated
@app.route("/secret")
@login_required
def secret():
return "Top secret content!"
Apply @login_required to any route — it’s
auto-protected.
Stretch — “Remember me” with session lifetime
from datetime import timedelta
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
# ... existing logic ...
if user and check_password_hash(...):
session.permanent = True
app.permanent_session_lifetime = timedelta(days=30)
session["user_id"] = user["id"]
...
Now sessions last 30 days even after browser close.
Stretch — change password
A new route — must be logged in, must verify old password:
@app.route("/change-password", methods=["GET", "POST"])
@login_required
def change_password():
if request.method == "POST":
old = request.form.get("old_password")
new = request.form.get("new_password")
user = current_user()
if not check_password_hash(user["password_hash"], old):
flash("Wrong current password.", "error")
else:
conn = get_db()
conn.execute(
"UPDATE users SET password_hash = ? WHERE id = ?",
(generate_password_hash(new), user["id"])
)
conn.commit()
conn.close()
flash("Password changed.", "success")
return redirect(url_for("home"))
return render_template("change_password.html")
Extension — email field, password reset
Real apps include an email column for password reset. Out of session scope (involves sending email — needs SMTP setup), but mention as the next step.
Extension — admin role
Add an is_admin column. Some routes only allow
admins:
if not current_user() or not current_user()["is_admin"]:
abort(403)
The basis of any role-based system.
Wrap-up
Before we leave, share with the room:
- For everyone — show your signup / login flow working. Try wrong password — does it fail cleanly?
- Did inspecting
password_hashin the database drive home why we hash? - For the kids who tried
@login_required— does the decorator pattern feel powerful? - Anyone notice that you can have multiple accounts in the same database now?
Today you learned:
- Never store plaintext passwords. Always hash.
werkzeug.securityhas the right functions:generate_password_hashandcheck_password_hash.- The
userstable with username, password_hash, created_at. UNIQUEconstraint on username — database prevents duplicates.- Signup → hash and INSERT.
- Login → SELECT user, check_password_hash,
set
session["user_id"]. - Logout →
session.pop. current_user()helper + context processor for templates.@login_requireddecorator to protect routes.
You can build any multi-user app now. The auth pattern is universal.
Next week: the notes app — combine auth + database + templates + forms into one complete multi-user web app. The full-stack moment.
If you missed this session
Open Thonny.
-
Build the full
app.pyfrom Part B. -
Build all four templates.
-
Test the signup → login → logout flow.
-
Inspect
auth.dbto see hashed passwords. -
(Stretch) Add
@login_required.
About 60-90 minutes — this is a substantial session.
Stretch and extension ideas
@login_requireddecorator for protecting routes.- Permanent sessions with custom lifetime.
- Change password route.
- Username availability check via fetch (Phase 7 callback) before submit.
- Account deletion (with password confirmation).
- Email field for future password reset.
- Role / admin column.
- Last login timestamp on the users table.
- Force HTTPS in production (configuration — out of session scope but real concern).
What’s next
Next week: build a notes app together — the full-stack integration. Auth + database + templates + forms = a real multi-user app where each user has their own private notes. Phase 5 and Phase 7 had todo apps; Phase 8 makes them multi-user.
Session 10: Build a notes app together — foundation
Session 10: Build a notes app together — foundation
Phase 8 — Flask · Session 10 of 14
What we’re learning today
You have all the pieces. Today they start to combine into a multi-user notes app — each user has their own private notes. Built as a class.
This session builds the foundation:
two-table database, auth from Session 9 wired
in, list notes, create notes. The crucial
per-user data pattern (WHERE user_id = ?)
gets its own dedicated time today.
Next session: view, edit, delete, and the multi-user demo.
You’ll need to remember from last time
- Auth flow — signup, login, session, password hashing (Session 9).
- Database CRUD — get_db, parameterized queries (Session 8).
- Templates with extends/block (Session 4).
- Forms with POST-Redirect-GET (Session 5).
- Flash messages (Session 6).
@login_requireddecorator (Session 9 stretch).
Part A: Set up the foundation
What we’re building today
- Signup / login / logout (from Session 9).
- Notes table with user ownership.
- List your notes (only yours).
- Create a note (title + content).
- Login required for everything except signup/login.
The shape of countless real apps — Trello, Notion, journals, blogs — all start here. View, edit, and delete come next session.
Project structure
notes_app/
├── app.py
├── notes.db
├── templates/
│ ├── base.html
│ ├── home.html
│ ├── signup.html
│ ├── login.html
│ ├── notes_list.html
│ └── note_form.html
└── static/
└── style.css
Database schema
Two tables. The users table from Session 9, plus
a notes table with a foreign key:
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
The user_id column links each note to a
user. The FOREIGN KEY says “this column points
to the id column of the users table.”
When listing a user’s notes, we filter by
WHERE user_id = ?. This filter is the most
important line of code in this whole app. We’ll
come back to it.
app.py — foundation
from flask import Flask, render_template, request, redirect, url_for, session, flash, abort
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import sqlite3
app = Flask(__name__)
app.secret_key = "dev-secret"
def get_db():
conn = sqlite3.connect("notes.db")
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
""")
conn.commit()
conn.close()
init_db()
def current_user():
user_id = session.get("user_id")
if not user_id:
return None
conn = get_db()
user = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
conn.close()
return user
@app.context_processor
def inject_user():
return {"current_user": current_user()}
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user():
flash("Please log in.", "error")
return redirect(url_for("login"))
return f(*args, **kwargs)
return decorated
What’s new:
conn.executescript(...)— run multiple SQL statements at once. Used here to create both tables in one call.
Auth routes (from Session 9)
Add the same signup, login, logout routes as Session 9. (Skipping repetition here — copy from Session 9.)
Checkpoint: Your foundation has both tables created and auth working. You can sign up, log in, and log out, but there are no notes routes yet. This is the natural stop point if class is cut short.
Part B: List and create notes
List notes — the per-user query
@app.route("/notes")
@login_required
def notes_list():
user = current_user()
conn = get_db()
notes = conn.execute(
"SELECT * FROM notes WHERE user_id = ? ORDER BY updated_at DESC",
(user["id"],)
).fetchall()
conn.close()
return render_template("notes_list.html", notes=notes)
Two crucial pieces:
@login_required— must be logged in.WHERE user_id = ?— only this user’s notes.
Stop and read that filter again. This query
is the entire pattern of per-user data. Same
notes table for every user — but the filter
narrows it to this user’s rows. Without the
filter, every user would see everyone’s
notes. That’s a security catastrophe. The
WHERE user_id = ? is non-negotiable on every
notes query you write.
templates/notes_list.html:
{% extends "base.html" %}
{% block title %}Your notes{% endblock %}
{% block content %}
<h1>Your notes</h1>
<a href="{{ url_for('note_create') }}" class="btn">+ New note</a>
{% if notes %}
<ul class="notes">
{% for note in notes %}
<li>
<h3>{{ note.title }}</h3>
<p>{{ note.content[:100] }}{% if note.content|length > 100 %}…{% endif %}</p>
<small>{{ note.updated_at }}</small>
</li>
{% endfor %}
</ul>
{% else %}
<p>No notes yet. <a href="{{ url_for('note_create') }}">Create your first one</a>.</p>
{% endif %}
{% endblock %}
Note note.content[:100] — first 100 characters
as a preview. Jinja supports Python slicing.
(We’re not linking to a “view” page yet — that’s next session. For now, the list shows previews.)
Create a note
@app.route("/notes/new", methods=["GET", "POST"])
@login_required
def note_create():
if request.method == "POST":
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if not title or not content:
flash("Both title and content required.", "error")
else:
user = current_user()
conn = get_db()
conn.execute(
"INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)",
(user["id"], title, content)
)
conn.commit()
conn.close()
flash("Note created!", "success")
return redirect(url_for("notes_list"))
return render_template("note_form.html", note=None)
Look at the INSERT carefully:
"INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)",
(user["id"], title, content)
We hardcode user_id = current user's id.
The user cannot set this to someone else’s id
by tampering — it’s not in the form. If user_id
came from a hidden form field, users could edit
their browser’s HTML and assign notes to other
people. Always set user_id from
current_user(), never from form data. Real
apps have been compromised this way.
templates/note_form.html (the same template
will get reused next session for editing — pass
note=None for create, the row for edit):
{% extends "base.html" %}
{% block title %}{% if note %}Edit{% else %}New{% endif %} note{% endblock %}
{% block content %}
<h1>{% if note %}Edit{% else %}New{% endif %} note</h1>
<form method="post">
<label>Title:
<input type="text" name="title"
value="{{ note.title if note else '' }}" required>
</label>
<label>Content:
<textarea name="content" rows="10" required>{{ note.content if note else '' }}</textarea>
</label>
<button type="submit">Save</button>
<a href="{{ url_for('notes_list') }}">Cancel</a>
</form>
{% endblock %}
The value="{{ note.title if note else '' }}"
handles both new (no note) and edit (with
note) — Jinja short-circuits.
Update the home page
templates/home.html:
{% extends "base.html" %}
{% block content %}
{% if current_user %}
<h1>Welcome, {{ current_user.username }}!</h1>
<a href="{{ url_for('notes_list') }}" class="btn">Your notes</a>
{% else %}
<h1>Welcome!</h1>
<p>A simple note-taking app. Sign up or log in to start.</p>
{% endif %}
{% endblock %}
Test it
- Sign up. Log in.
- Click “Your notes.” Empty list.
- Click “+ New note.” Type a title and content. Save. Note appears in the list.
- Create a few more.
Now log out. Sign up as a different user.
Log in. Click “Your notes.” Empty — your
notes are yours. The first user’s notes don’t
appear here. The WHERE user_id = ? filter is
working.
You just built the core of every multi-user app ever written. That filter is the difference between a journal app and a journal-app-shaped security incident.
Checkpoint: Your notes app has signup, login, list, and create — all filtering by user. This is the natural stop point if class is cut short.
Wrap-up
Before we leave, share with the room:
- For everyone — show your notes app working. Create a note. List it.
- For the kids who tested with two users — show off the per-user isolation.
- Did the per-user filter feel important? It should. Every multi-user app you ever build rests on it.
Today you built the foundation of a real full-stack app:
- Two tables with a foreign key
- Auth wired in (from Session 9)
- The per-user data pattern (
WHERE user_id = ?) - List and create routes
Next week we add view, edit, delete, do the multi-user security demo, and explore the stretches (search, tags, public notes).
If you missed this session
Open Thonny.
-
Build the foundation from Part A — both tables, auth (copy from Session 9).
-
Build the list and create routes from Part B.
-
Build the templates.
-
Test the full flow: signup, login, list, create. Try with two users.
About 60-90 minutes.
Stretch and extension ideas
- Sort options — newest first vs alphabetical.
- Empty-state styling — make the “no notes yet” page friendlier.
- Character counter in the new-note form (Phase 7 JS).
- Note count in the page heading: “Your 5 notes.”
- Read ahead — peek at the spec for next session and try edit / delete on your own.
What’s next
Next week: view, edit, delete, and the multi-user security demo. We finish the notes app and try to break it (in the good way) to prove the per-user filter holds.
Session 11: Build a notes app together — polish
Session 11: Build a notes app together — polish
Phase 8 — Flask · Session 11 of 14
What we’re learning today
Last session we built the notes app foundation: auth, schema, list, create. Today we finish the app — view a note, edit it, delete it — and then try to break it (in the good way) to prove our per-user filter actually holds.
This session is the integration moment. By the end you have a working full-stack multi-user app. The shape of every modern web app: Trello, Notion, journals, blogs.
You’ll need to remember from last time
- Your notes app from Session 10 — auth, schema, list, create.
- The
WHERE user_id = ?filter. Every notes query has it. We add it to three more routes today. abort()from Flask — return a 404 from inside a route.
Part A: View, edit, and delete
View a single note
@app.route("/notes/<int:note_id>")
@login_required
def note_view(note_id):
user = current_user()
conn = get_db()
note = conn.execute(
"SELECT * FROM notes WHERE id = ? AND user_id = ?",
(note_id, user["id"])
).fetchone()
conn.close()
if not note:
abort(404)
return render_template("note_view.html", note=note)
The crucial query: WHERE id = ? AND user_id = ?.
We check both the note ID and that it belongs
to the current user. Without the user_id
check, any logged-in user could view any note
by changing the number in the URL — security
hole.
We’ll demo this in Part B.
templates/note_view.html:
{% extends "base.html" %}
{% block title %}{{ note.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ note.title }}</h1>
<p>{{ note.content }}</p>
<small>Created {{ note.created_at }} · Updated {{ note.updated_at }}</small>
</article>
<div class="actions">
<a href="{{ url_for('note_edit', note_id=note.id) }}" class="btn">Edit</a>
<form method="post" action="{{ url_for('note_delete', note_id=note.id) }}"
onsubmit="return confirm('Delete this note?');"
style="display: inline;">
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<a href="{{ url_for('notes_list') }}">Back to list</a>
</div>
{% endblock %}
The onsubmit="return confirm(...)" is JS from
Phase 7 — confirmation before destructive
action.
Now link the list to the view. Open
notes_list.html and wrap each note’s title in
an anchor:
<li>
<a href="{{ url_for('note_view', note_id=note.id) }}">
<h3>{{ note.title }}</h3>
<p>{{ note.content[:100] }}{% if note.content|length > 100 %}…{% endif %}</p>
<small>{{ note.updated_at }}</small>
</a>
</li>
Edit a note
@app.route("/notes/<int:note_id>/edit", methods=["GET", "POST"])
@login_required
def note_edit(note_id):
user = current_user()
conn = get_db()
note = conn.execute(
"SELECT * FROM notes WHERE id = ? AND user_id = ?",
(note_id, user["id"])
).fetchone()
if not note:
conn.close()
abort(404)
if request.method == "POST":
title = request.form.get("title", "").strip()
content = request.form.get("content", "").strip()
if not title or not content:
flash("Both fields required.", "error")
else:
conn.execute(
"""UPDATE notes
SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?""",
(title, content, note_id, user["id"])
)
conn.commit()
conn.close()
flash("Note updated.", "success")
return redirect(url_for("note_view", note_id=note_id))
conn.close()
return render_template("note_form.html", note=note)
Same pattern as Session 8’s edit — GET shows
the form pre-filled, POST saves changes. Same
AND user_id = ? check to prevent editing
others’ notes — and notice it appears twice
in this route (once on the SELECT, once on the
UPDATE). Both matter.
updated_at = CURRENT_TIMESTAMP updates the
last-modified time. The template (note_form.html)
is the same template you wrote last week —
it already pre-fills when note is passed.
Delete a note
@app.route("/notes/<int:note_id>/delete", methods=["POST"])
@login_required
def note_delete(note_id):
user = current_user()
conn = get_db()
conn.execute(
"DELETE FROM notes WHERE id = ? AND user_id = ?",
(note_id, user["id"])
)
conn.commit()
conn.close()
flash("Note deleted.", "success")
return redirect(url_for("notes_list"))
POST only (Session 8 rule — destructive actions are POST, not GET). Same user-check.
Test it
- Log in. Open the notes list.
- Click a note. View page loads.
- Click Edit. Form pre-fills. Change the title. Save. Updated time changes.
- Click Delete. Confirmation appears. Confirm. Returns to list, note is gone.
Checkpoint: Your notes app has list, create, view, edit, and delete — all filtering by user. This is the natural stop point if class is cut short.
Part B: Multi-user demo + try to break it
The multi-user demo
This is the moment the app feels real. Try it with the kid next to you:
- You sign up as User A. Your buddy as User B. (Same app, two accounts — different browsers, or one browser plus an incognito window.)
- Each create some notes.
- Log out as A. Log in as B. Look at “Your notes” — only B’s notes show. A’s notes don’t appear. Same for the reverse.
Two users. Different data. Same app. This is the magic. Per-user state, real persistence, real isolation.
Now try to break it
Real engineers test their security by trying to get past it. Let’s do that to your app:
- Log in as User B.
- Create a note. Click into it. Look at
the URL — something like
http://127.0.0.1:5000/notes/3. Note the number. - Now visit a smaller number — say,
/notes/1. (User A’s notes have ID 1 and 2; you’re guessing.) - You get a 404 page. Even though note 1
exists in the database, the
AND user_id = ?filter excludes it. The query returns no rows →abort(404).
Try the same trick on /notes/1/edit. Same
404. The check holds on edit too.
That’s the security demo. Even if a user guesses the right ID, the per-user filter protects. This is real, and it’s the same pattern that protects your bank account from showing other people’s data.
What if you forgot the filter? Try this for real (then put it back!):
note = conn.execute(
"SELECT * FROM notes WHERE id = ?", # NO user_id check
(note_id,)
).fetchone()
Now log in as B. Visit /notes/1. You see
A’s note. That’s the security hole the filter
prevents.
Put the filter back when you’re done testing.
What you have now
- Auth (signup, login, logout)
- Per-user data with
WHERE user_id = ?on every query - Full CRUD: list, create, view, edit, delete
- Templates with extends/block
- Forms with POST-Redirect-GET
- Flash messages
- A real multi-user web app
This is the entire shape of every modern web app. Whatever you build next — bigger, fancier, in different languages — the patterns are the same.
Wrap-up
Before we leave, share with the room:
- For everyone — show the multi-user demo with your buddy. Two accounts, isolated data.
- For the kids who tried the URL-tampering break — was the protection satisfying? It should be. Real apps survive on this.
- Did the integration feel powerful? All the pieces working together.
Next week: deployment. Get your notes app on a real public URL. Anyone can sign up.
If you missed this session
Open Thonny. Start with your Session 10 notes app foundation.
-
Add the view, edit, and delete routes from Part A.
-
Update
notes_list.htmlto link to the view page. -
Test the full flow: list, create, view, edit, delete.
-
Try the URL-tampering test with two accounts.
About 60-90 minutes.
Stretch and extension ideas
Search
Add ?q=word to filter notes:
search = request.args.get("q", "").strip()
if search:
notes = conn.execute(
"""SELECT * FROM notes
WHERE user_id = ? AND (title LIKE ? OR content LIKE ?)
ORDER BY updated_at DESC""",
(user["id"], f"%{search}%", f"%{search}%")
).fetchall()
else:
notes = conn.execute(
"SELECT * FROM notes WHERE user_id = ? ORDER BY updated_at DESC",
(user["id"],)
).fetchall()
Add a search box to notes_list.html:
<form method="get" action="{{ url_for('notes_list') }}">
<input type="search" name="q" placeholder="Search notes..."
value="{{ request.args.get('q', '') }}">
<button type="submit">Search</button>
</form>
Tags
Add a tags table and a note_tags join table.
Real database design:
CREATE TABLE tags (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
name TEXT NOT NULL
);
CREATE TABLE note_tags (
note_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (note_id, tag_id)
);
Query notes with their tags via JOIN.
Public notes
A is_public column. Public notes are readable
without login. URL: /public/<note_id>. No
user_id check for public notes — but only on
that one route. The user’s own views still
filter by user_id.
Pagination
LIMIT 20 OFFSET ? for big lists. Add page
controls to the template.
Rich text (Markdown)
Replace plain <textarea> with Markdown. Save
Markdown, render with a Markdown library:
import markdown
note["html"] = markdown.markdown(note["content"])
In template:
<div class="content">{{ note.html|safe }}</div>
(|safe is needed because Jinja escapes by
default. Markdown output is intended HTML.)
Export
A “Download all my notes” button. Generates a JSON file:
from flask import send_file
import json
import io
@app.route("/notes/export")
@login_required
def export_notes():
user = current_user()
conn = get_db()
notes = conn.execute(
"SELECT * FROM notes WHERE user_id = ?", (user["id"],)
).fetchall()
conn.close()
data = [dict(note) for note in notes]
json_str = json.dumps(data, indent=2)
return send_file(
io.BytesIO(json_str.encode()),
as_attachment=True,
download_name="notes.json",
mimetype="application/json"
)
Sharing (advanced)
A shared_notes table:
CREATE TABLE shared_notes (
note_id INTEGER,
shared_with_user_id INTEGER,
PRIMARY KEY (note_id, shared_with_user_id)
);
User A shares a note with User B. User B sees it in a “Shared with me” view. Real collaboration.
Other directions
- Categories / folders for notes.
- Star / favorite notes.
- Most recent N on home page.
- Stats — how many notes, words, etc.
- Export individual note as text or PDF.
- API endpoint that returns notes as JSON (Phase 7 fetch could call this).
What’s next
Next week: deployment. PythonAnywhere — free Python hosting designed for educators and students. Walk through signup, upload code, configure, hit the public URL. Your notes app becomes a real internet thing — anyone can sign up.
Session 12: Deployment — your Flask app on the internet
Session 12: Deployment — your Flask app on the internet
Phase 8 — Flask · Session 12 of 14
What we’re learning today
Your notes app runs on 127.0.0.1:5000 — only
your computer can reach it. Today we put it
on the internet — at a real public URL where
anyone with the link can sign up and use it. We’ll
deploy to PythonAnywhere — a free hosting
service designed for Python apps. By the end your
notes app will be at a <username>.pythonanywhere.com
URL.
This is the moment your back-end becomes public.
You’ll need to remember from last time
- The notes app from Sessions 10-11 (or any Flask app you want to deploy).
- Git basics — clone, push.
- PythonAnywhere account (we’ll create one today).
Part A: Deploy the notes app
Why PythonAnywhere?
Static sites (Phase 7) deploy to GitHub Pages — no server needed. Flask apps need a running Python process, which GitHub Pages can’t do.
We need a Python hosting service. Options:
- PythonAnywhere — free tier, no credit card, designed for educators. Easy web UI. Our pick.
- Render — generous free tier, modern. But needs more terminal/command-line.
- Railway, Fly.io, Vercel — varies.
PythonAnywhere is the kindest entry point for beginners.
Step 1 — Create an account
- Go to pythonanywhere.com.
- Click Pricing & signup.
- Pick the “Beginner” account (free).
- Fill in username + email + password.
- Verify your email.
Your username becomes part of your URL:
<username>.pythonanywhere.com. Pick something
you’re OK with for years.
Step 2 — Get your code on PythonAnywhere
There are two ways: upload a zip, or git clone
from GitHub. Git clone is the right way —
keeps your code in sync.
If your notes app isn’t on GitHub yet:
- Push it to GitHub (Phase 6 Session 7 refresher).
In PythonAnywhere:
- Click the Consoles tab.
- Start a Bash console.
- In the console:
$ git clone https://github.com/YOUR-USERNAME/notes_app.git cd notes_appandlsto verify your files are there.
Step 3 — Set up the web app
- Click the Web tab.
- Click Add a new web app.
- Click Next through the domain setup
(free accounts get
<username>.pythonanywhere.com). - Pick “Manual configuration” (not “Flask” — the auto-Flask option is for old setups).
- Pick Python 3.10 (or the latest available).
- Click Next.
Now you’re on the configuration page for your web app.
Step 4 — Configure WSGI
This is the trickiest step. Find the Code section with “WSGI configuration file.” Click the link to edit it.
Replace its contents with:
import sys
import os
# Update this to your username and project folder
project_home = "/home/YOUR-USERNAME/notes_app"
if project_home not in sys.path:
sys.path.insert(0, project_home)
os.chdir(project_home)
from app import app as application
Walk through:
project_home— where your code lives on PythonAnywhere. ReplaceYOUR-USERNAMEwith your PythonAnywhere username.sys.path.insert(0, project_home)— tells Python where to find your app.os.chdir(project_home)— so SQLite finds the DB file in the right folder.from app import app as application— imports your Flask app. PythonAnywhere expects the app to be calledapplication.
Save the WSGI file.
Step 5 — Install Flask in PythonAnywhere
In the Bash console:
$ pip install --user flask
The --user flag installs to your user directory
(no system permission needed).
Step 6 — Reload the web app
Back in the Web tab, scroll up. Click the big green Reload button.
Wait 5-10 seconds.
Step 7 — Visit your URL
The URL is shown at the top of the Web tab —
<username>.pythonanywhere.com. Click it.
Your notes app loads.
Sign up. Log in. Create a note. Real public URL. Send the link to a friend — they can sign up too.
You shipped a real multi-user web app to the internet.
What about updates?
When you change your code locally:
- Commit and push to GitHub.
- On PythonAnywhere, in the bash console:
$ cd notes_app $ git pull - In the Web tab, click Reload.
- Visit the URL — change is live.
A bit more manual than GitHub Pages (which auto-deploys on push), but workable.
When your URL doesn’t load
Real talk: deployments break. The first time you deploy a Flask app, most of you will hit some error. That’s normal. The fix is always the same two-step move: find the error message, fix what it says.
Step 1 — find the error log. PythonAnywhere keeps one for every web app. In the Web tab, scroll to the “Log files” section. Click the “Error log” link. The most recent entries are at the bottom. The actual problem is usually in the last few lines — Python’s traceback.
Step 2 — read the last line of the traceback. That’s the actual error. Common ones:
ModuleNotFoundError: No module named 'flask'→ You skippedpip install --user flask(Step 5 above). Run it. Reload.ModuleNotFoundError: No module named 'app'→ Your WSGI file’sproject_homepath is wrong. Fix the path. Reload.No such file or directory: 'notes.db'→ SQLite can’t find your database file. The fix is the absolute-path trick from Part B (os.path.join(os.path.dirname(__file__), "notes.db")). Push, pull, reload.Something went wrong :-(with no traceback. → The Python version mismatch (you picked Python 3.10 in setup but PythonAnywhere defaulted to a different one in the Web tab). Check the “Python version” dropdown in the Web tab.
Step 3 — fix it locally first. Don’t edit code
on PythonAnywhere directly. Edit on your own
machine (where you can test), commit, push,
git pull on PythonAnywhere, reload. Always.
Two more callbacks for “did you forget?” — Did you push your latest commit to GitHub? Did you
git pullon PythonAnywhere after? Did you click Reload in the Web tab? All three are required for your changes to show up. Forgetting any one of them is the most common deploy frustration.
You’ll get fast at this loop with practice. Real production engineers do this exact dance every day.
Checkpoint: Your notes app is at a public URL. Anyone can sign up. This is the natural stop point if class is cut short. (Today’s goal.)
Part B: Make it production-ready (basics)
Set a real secret_key
Your local app.secret_key = "dev-secret" is
fine for class but ridiculous for production.
Use an environment variable:
import os
app.secret_key = os.environ.get("SECRET_KEY", "dev-secret")
In PythonAnywhere, set the env var. In the
Files tab, edit ~/.bashrc and add:
export SECRET_KEY="some-long-random-string"
Or (PythonAnywhere-specific) in the Web tab, scroll to Environment variables and add it there.
For class: optional polish. Real apps require it.
Update the database file path
Your sqlite3.connect("notes.db") uses a
relative path. PythonAnywhere may run from a
different directory. Use an absolute path:
import os
DB_PATH = os.path.join(os.path.dirname(__file__), "notes.db")
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
__file__ is the current Python file’s path.
os.path.dirname(__file__) is its folder. Joining
with “notes.db” gives a full path.
This just works regardless of working directory.
Add a real requirements.txt
A standard Python file listing your dependencies.
Create requirements.txt:
Flask
(Just one line for now. Real apps list every package.)
Then on PythonAnywhere:
$ pip install --user -r requirements.txt
Installs everything in the file. Real-project practice.
Add a .gitignore
You shouldn’t commit the database file (it has
production data) or __pycache__:
.gitignore:
*.db
__pycache__/
.env
If you’ve already committed notes.db, remove
it from Git tracking:
$ git rm --cached notes.db
$ git commit -m "Stop tracking db"
$ git push
Now the database stays on PythonAnywhere only — not in your GitHub repo.
Stretch — turn off debug mode
In production:
if __name__ == "__main__":
app.run(debug=False)
Debug mode shows error tracebacks to users —
never in production. PythonAnywhere ignores
your app.run() (it uses WSGI), so this is
fine for class. Still good practice.
Stretch — disable signup
If you don’t want random people signing up to your app, comment out the signup route or add an invite-code check.
Stretch — share with your class
Each student deploys. Pass URLs around. Sign up on each other’s apps. Real users for real apps.
Stretch — log to a file
For debugging deployed issues:
import logging
logging.basicConfig(filename="app.log", level=logging.INFO)
@app.route("/")
def home():
app.logger.info("Home accessed")
return ...
In PythonAnywhere, view the log file in the Files tab. Real production debugging.
Extension — custom domain
If you have a domain (~$10/year), you can point it at your PythonAnywhere app. Free tier supports it. Out of class scope — but worth knowing.
Extension — try other hosts
For curiosity, try Render or Fly.io. Each has a free tier. Different deployment flow (more git-based, less UI). Real-world practice.
Wrap-up
Before we leave, share with the room:
- For everyone — show your URL. Open it on the projector. Sign up, create a note.
- Have a classmate visit your URL on their machine. Two-person test of your app.
- Did the deploy feel like a moment? Your back-end is on the internet now.
- For the kids who set up their
.gitignoreand absolute paths — was the polish worth it?
Today you learned:
- Static hosting (GitHub Pages) doesn’t work for Flask — needs a running Python process.
- PythonAnywhere — free Python hosting for beginners.
- WSGI file — tells the host how to find your app.
--userinstall for dependencies.git pull+ Reload for updates.- Absolute paths (
__file__-based) for database files. .gitignoreto keep db files out of Git.- Environment variables for secrets.
requirements.txtfor dependency tracking.
Your back-end is on the internet. Anyone with the URL can sign up and use your app. That’s real shipping.
Next two weeks: your milestone full-stack app. Your design, your code, your URL.
If you missed this session
Sign up at pythonanywhere.com. Then:
-
Push your notes app to GitHub if you haven’t.
-
In PythonAnywhere bash console:
git cloneyour repo. -
Add a new web app (Manual configuration, Python 3.10).
-
Edit the WSGI file to point at your folder and import your
app. -
pip install --user flask. -
Reload the web app.
-
Visit the URL.
About 45-60 minutes. By the end you should have a public URL.
Stretch and extension ideas
- Real
secret_keyfrom env var. - Absolute path for DB.
requirements.txtwith all deps..gitignorefor*.db,__pycache__/.- Disable debug in production.
- Logging to a file.
- Custom domain (if you own one).
- Try Render or Fly.io for comparison.
- HTTPS only redirect.
What’s next
Next two weeks are yours. You plan and build a full-stack milestone — your design, your data, your users. By Session 14’s demo, your app will be at a real URL anyone in the world can visit.
Bring an idea. Or come empty-handed — we’ll have a seed list.
Session 13: Milestone project work day 1
Session 13: Milestone project work day 1
Phase 8 — Flask · Session 13 of 14
What we’re learning today
Today is your day. You’ll plan a full-stack web app — your design, your data, your users — and start building it. Next week you finish, deploy, and demo your live URL to the class. This is your eighth and final milestone of the curriculum.
You’ll need to remember from last time
- Templates — extends/block, partials.
- Database — get_db, CRUD, parameterized queries.
- Auth — signup, login, hashed passwords, current_user.
- Forms — POST-Redirect-GET, validation.
- Flash messages for feedback.
- Deployment — PythonAnywhere.
Part A: Plan your project
The plan
Take paper or open a blank text file. Answer these nine questions:
-
What’s the app? (One sentence. “A recipe-sharing site.” “A polling app.” “A reading log.”)
-
Who’s it for? (Yourself? Class? General public?)
-
Does it need user accounts? (Almost always yes for Phase 8 milestones.)
-
What does each user do? (Sign up, log in, then… what?)
-
What database tables? (
usersplus what? Each table: name + columns.) -
What routes? (Each URL + what it does. At minimum: home, signup, login, logout, list, create, view, edit, delete.)
-
What templates? (One per route, usually.)
-
What’s the simplest version? (Build this FIRST. No fancy features. Just core flow.)
-
What’s one stretch feature? (After core works.)
Phase 8 requirements
Your milestone must:
- Use HTML + CSS + Python (Flask).
- Have user accounts (signup, login, logout with hashed passwords).
- Have a database with at least 2 tables
(
users+ one other). - Have full CRUD for at least one entity (create, read, update, delete).
- Be deployed to a public URL by the demo — PythonAnywhere or another host.
- Be usable by multiple users — your classmates should be able to sign up and use it.
Bonus:
- Extra tables / relationships.
- Search / filter.
- Public + private content.
- File uploads.
- API endpoints (JSON returns).
- Real CSS polish.
If you don’t have an idea
Pick one and modify:
Personal apps (your data only):
- Reading log — track books, ratings, reviews.
- Habit tracker — daily check-in for habits.
- Goal tracker — long-term goals with progress.
- Workout log — exercises, sets, dates.
- Movie / show watchlist — to-watch and watched.
- Recipe collection — categorized, with ingredients.
- Quote collection — favorite quotes by source.
- Daily journal — date-tagged entries.
Multi-user apps (community):
- Class profile site — each kid has a card with their interests.
- Class wiki — shared editable pages.
- Recipe sharing — submit and browse others’ recipes.
- Book club — vote on next book, discuss.
- Polls / voting — create polls, vote, see results.
- Q&A site — ask questions, others answer.
- Photo gallery with comments.
- Trivia game — questions, scores per user, leaderboard.
Game / interactive apps:
- Multi-user trivia with high scores per user.
- Word game — guess the word, score per user.
- Memory game with persistent best times.
- Choose-your-own-adventure with saved progress.
Tools / productivity:
- Group todo list — shared, multi-user permissions.
- Class scheduler — assignments + due dates.
- Note-taking (extension of Sessions 10-11).
- Bookmark manager — save URLs with tags.
- Pomodoro / time tracker with stats.
Variants of in-class projects:
- The notes app from Sessions 10-11 with new features (tags, search, public mode, sharing).
- A guestbook (Session 5/8) + accounts so posters are users.
Pick one. Spend two minutes. Don’t overthink.
Show Mr. Eric
When your plan is ready, show it to Mr. Eric. He’ll either say “go build it” or ask one question.
Set up the project + deploy day 1
$ mkdir my_app && cd my_app
$ touch app.py
$ git init
$ git add app.py
$ git commit -m "Initial setup"
Push to GitHub:
- Create a new public repo on GitHub.
git remote add origin ...+git push -u origin main.
Then deploy to PythonAnywhere right now, before any real code:
- PythonAnywhere bash console:
git cloneyour repo. - Add a new web app (Manual config, Python 3.10).
- Edit WSGI file.
pip install --user flask.- Reload.
- Visit URL — should show some kind of error (no code yet) or “Hello world” if you wrote one.
Why deploy day 1? Same reason as Phase 7
Session 16: catch deployment issues now, not
the night before demo. Every commit can be
deployed via git pull + Reload.
Build the simplest version first
Look at your answer to question 8. Build that first. Get a window on screen — even just signup
- login working. Then iterate.
For each significant change:
- Make the change locally.
- Test in browser at
127.0.0.1:5000. - Commit + push.
- PythonAnywhere bash:
cd my_app && git pull. - PythonAnywhere web tab: Reload.
- Verify on the public URL.
This is the real deploy loop. Do it for every significant change.
Use what you’ve learned
- Auth from Session 9.
- Multi-table database from Sessions 10-11.
- Templates with extends from Session 4.
- Forms with POST-Redirect-GET from Session 5.
- Flash messages from Session 6.
- Per-user data isolation with
WHERE user_id = ?from Sessions 10-11.
The complete patterns are in Sessions 1-12.
Wrap-up
Last 5 minutes: each of you, in one sentence, share your project idea + your live URL. (URL might just show “Hello world” right now — that’s OK.)
Bring your project (code + URL) next week. We’ll finish, then demo.
If you got far, commit, push, pull, reload before next week so the public URL is current.
If you missed this session
Open Thonny and a terminal.
-
Spend 10-15 minutes answering the nine planning questions.
-
Set up your project + GitHub + PythonAnywhere.
-
Build the simplest version (signup + login at minimum).
-
Push and deploy.
About 90-120 minutes total. By next week:
- Working signup and login at a public URL.
- At least one feature beyond auth (e.g., a list of items the user has created).
- Several Git commits.
- The deploy loop comfortable.
If you don’t have an idea, the seed list is your starting point.
Stretch and extension ideas
If your base is working and you want to add more:
- Real CSS polish (Phase 7 callback).
- Mobile-responsive design.
- Search / filter for your data.
- Public + private modes for content.
- Multiple data types / tables — relationships.
- Image uploads.
- API endpoints that return JSON.
- Email notifications (advanced).
- README with screenshots.
- Cross-link from your Phase 7 homepage — unified portfolio.
What’s next
Next week is the last session of the entire curriculum. You’ll finish, polish, deploy your final version, and demo to the class.
Bring your laptop. Bring your URL. Bring your enthusiasm. Last big climb.
Session 14: Milestone project work day 2 + demo + curriculum close
Session 14: Milestone day 2 + demo + curriculum close
Phase 8 — Flask · Session 14 of 14 — last session of the entire curriculum
What we’re learning today
This is the last session. Eight phases. Eight milestones. Today: finish your full-stack app, deploy your final version, demo your live URL to the class. Your classmates sign up. Your work is on the internet for real.
Then we close out the journey.
You’ll need to remember from last time
- Your project plan from last week.
- Your deployed URL.
- Git push + PythonAnywhere reload for updates.
- Everything from Phases 1-8.
Part A: Final polish
You have about 35 minutes to finish.
What “finished” means
Same three rules as always:
- It runs without crashing under normal use.
- It does what your plan said — at least the simplest version.
- You can explain how it works.
Phase 8-specific polish
- Verify the live URL works. Push, pull, reload, visit. Don’t assume.
- Test signup as a fresh user. From your phone, or a different browser, or incognito mode.
- Check error log on PythonAnywhere — any errors? Fix.
- Test on a phone if you can.
- Window title —
<title>tag. - Real
secret_keyfrom env var if you want production polish. .gitignorefor*.dbso production data isn’t in your repo.- README on the repo with description, live URL, screenshots if you have them.
- All committed and pushed.
git statusshould say “working tree clean.” - Pulled and reloaded on PythonAnywhere.
Buddy test
About 15 minutes in: swap URLs with your buddy. They sign up on your app. They use it. You do the same.
For Phase 8 specifically: your buddy is now a real user of your real app. Their data joins yours in the same database. Real multi-user testing.
Notice things they get wrong (control confusion, unclear next step). Real UX.
Checkpoint: Your project runs at a live URL, your buddy signed up successfully and used it. This is the natural stop point if class is cut short — but today, demo time is next.
Part B: Demo day + curriculum close
Each person gets 5-7 minutes (longer than usual — last demo).
How a demo works
When it’s your turn:
- Show your URL. Open it on the projector.
- Sign up live as Mr. Eric (or someone else fresh).
- Use your app. Walk us through what it does.
- Have a classmate sign up on their machine during your demo.
- Show your repo. GitHub URL,
git log. - Tell us about it. What it is, who it’s for, what was hard.
- Show the database if you can (DB Browser locally, or a screenshot).
- One question from the audience.
For your last demo: take your time. You built something real.
After everyone demos
Mr. Eric will say specific things about each project. Then we close out the curriculum.
What you accomplished — this phase
- A full-stack web app. Auth + database + templates + UI.
- Deployed to a public URL. Real users signed up.
- Eighth milestone shipped.
What you accomplished — the whole curriculum
Let’s count.
You shipped eight projects across eight phases. Most adults have shipped zero. You have eight. Three of them are on the internet.
The phases:
- Scratch — visual programming. Sequences, loops, events, conditionals, variables.
- Python Turtle — typing instead of dragging. Functions. Same ideas, real syntax.
- Python basics — strings, lists, dicts, files, error handling. The full beginner Python.
- Intermediate Python + CLI + Git — multi-file programs, classes, version control, testing.
- customtkinter — desktop GUIs. Real apps with windows, buttons, forms.
- Pygame — 2D games. Frame loop, sprites, collisions, sound, your custom grid-world.
- HTML/CSS/JavaScript — the front-end. The browser as runtime.
- Flask — the back-end. Python on a server. Multi-user apps.
You’ve covered:
- Three programming languages — Python, JavaScript, plus HTML/CSS as markup languages.
- Three runtimes — desktop (Pygame, customtkinter), browser (HTML/JS), server (Flask).
- Real software engineering practice — Git, GitHub, deployments, multi-file organization, testing.
- Real security practice — password hashing, parameterized queries, per-user data isolation.
- Design patterns that show up in every app you’ll ever build.
You’re a real software engineer. Not yet expert — but you have the foundation. From here, every framework, every language, every specialty is more learning on top of solid ground.
What’s next?
This curriculum is over. Your career as a programmer is just starting.
Some directions to consider:
Keep building
The projects you built are yours. Keep adding to them. Add features. Polish. Share with more people.
The single best way to grow as a programmer is building things you care about. Pick a problem in your life. Build a tool that fixes it. Repeat.
A few seed projects to try this week
If you want a concrete next step (and momentum this week, while everything is fresh), pick one of these:
- A habit tracker. Like the notes app, but
tracks streaks — every day you check off “I
did the thing,” and the longest current
streak is shown. New table:
checkins. Same per-user filter, same auth. - A reading log. Title, author, date finished, rating, notes. List. Sort by date or rating.
- A bookmark manager. Save URLs with tags. Search by tag.
- Extend one of your milestones. Pick the milestone you’re proudest of and add one feature you wished you’d had time for.
- A tiny tool you actually need. A timer for chores, a calculator for something specific to your life, a checklist for your morning routine. Real things you’d use.
- Port a Phase 6 game to JavaScript. Phase 7 Sessions 12-13 covered Canvas. Take your Pygame milestone and rebuild it in the browser, where anyone can play.
- An API + a front-end. Build a Flask app
that returns JSON, and a Phase-7-style HTML
page that uses
fetchto load and display the data. Real full-stack, your design.
Pick one. Build the simplest version this weekend. Send Mr. Eric the URL when it works.
Contribute to open source
Browse github.com. Find projects you use (Pygame, Flask, anything). Read the README. Look at issues. Try fixing one.
Open-source contribution is how real programmers learn from each other. Free education at any level.
Learn a framework
JavaScript: try React or Vue. Browser apps with rich UIs.
Python: try Django (bigger than Flask, more “batteries included”) or FastAPI (modern API framework).
The patterns you learned here transfer. Frameworks add structure on top.
Pick a specialty
Some directions real engineers go:
- Web dev (front-end, back-end, full-stack) — what we did.
- Mobile (iOS with Swift, Android with Kotlin, cross-platform with React Native or Flutter).
- Game dev (Unity, Godot, Unreal — bigger than Pygame, but the same mental model).
- Data science / ML (Python continues — pandas, NumPy, scikit-learn, PyTorch).
- Systems (Rust, C, Go — closer to the metal).
- DevOps / Cloud (AWS, Docker, Kubernetes — running things at scale).
You don’t have to pick now. Try things. See what excites you.
Learn things outside programming
The best programmers know things besides programming. Math, design, music, writing, biology, history — anything you care about makes you a better engineer of solutions in that domain.
Being good at code AND good at one other thing beats being just good at code.
Read code
Real projects on GitHub. Frameworks. Open- source tools. Reading code is how you learn to write better code.
You can read any of them. That’s the magic of the web.
Stay curious
The single thing every great engineer has in common: curiosity. Why does this work? How does that work? What would happen if I changed this?
If you stay curious, you’ll be a programmer for life.
A note from Mr. Eric
You did the work. You showed up week after week. You built when it was hard. You shipped when it was scary. You demoed when you were nervous.
You earned this.
Whatever you build next, remember: every expert was once a beginner. The best engineers in the world started where you are. The only difference is they kept building.
Keep building.
What’s next (literally)
There’s no formal “Phase 9.” This is the end of the From Scratch Programming Class.
But programming has no end. You can:
- Keep extending your milestone projects.
- Pick a new project from the seed lists.
- Read MDN, Real Python, Pygame docs, Flask docs.
- Watch tutorials on YouTube (lots of free, high-quality content).
- Help a friend learn to code — teaching is the best way to deepen what you know.
If you ever want to come back and ask Mr. Eric a question — about code, about a project, about what to learn next — come back.
Final note
You’re a programmer.
Welcome.
Appendices
Reference material that doesn’t belong inside any single session — the kind of thing you’ll come back to over and over.
- Getting unstuck — the checklist for when your code isn’t doing what you want.
- Reading error messages — how to make sense of the red text Python gives you when something goes wrong.
- Glossary — every programming term we’ve used, defined briefly. Grows phase by phase.
- Keyboard shortcuts — what every key combination on the class machines does.
- Thonny reference — quick reference for the Python editor we use.
- Installing on your own computer — for setting up Python and the class tools at home.
Bookmark this page. Most of these appendices stay useful across multiple phases.
Getting unstuck
Stuck is normal. Programmers spend most of their day stuck. The skill isn’t avoiding stuck — it’s getting unstuck. This page is the checklist you come back to whenever your code isn’t doing what you want.
Work through the steps in order. Don’t skip ahead. Most of the time, you’ll fix the problem before you reach the end of the list, because the act of working through the steps is the fix.
Step 1: Read what you wrote, slowly
Before anything else, look at your code. Actually look at it. Read each block (or each line, in Python) out loud if you can.
Programmers — especially programmers who are starting out — often think they wrote one thing and actually wrote something else. Reading slowly catches the gap.
Specifically: did you tell the computer the exact thing you meant? Or did you tell it something close-ish?
Remember the peanut butter problem from Session 1. The computer doesn’t guess. It does what you said, even when what you said is wrong.
About half the time, reading slowly fixes the problem.
Step 2: Read the error message (when there is one)
In Scratch you don’t usually get error messages — things just don’t work. But once you’re in Python, error messages are everywhere, and they are almost always trying to help.
Find the error message. It probably has a few parts:
- The file name where the problem is.
- The line number where the problem is.
- What kind of problem (a name like
SyntaxError,TypeError,NameError, etc.). - A short description of what went wrong.
Read all of it. The most useful part is usually the last line of the error.
If the error message uses a word you don’t recognize — like
indentation or attribute or iterable — that’s a real clue.
Look it up.
Step 3: Narrow it down
When something doesn’t work, your goal is to figure out which part doesn’t work. Usually it’s not the whole program. Usually it’s one specific block or one specific line.
Two ways to narrow down:
- Comment things out. If your program does five things and one of them is broken, temporarily turn off four of them and see if the broken one still happens. (In Scratch, you can right-click a block and “Disable Block” to turn it off without deleting it.)
- Add a
sayblock (Scratch) or aprint()(Python) at different points in your program. If the message shows up, that part of the code ran. If it doesn’t show up, the problem is somewhere before that point.
This is called bisecting — cutting the problem in half over and over until you find the broken part.
Step 4: Rubber-duck it
Find a friend, your buddy, your dog, or — yes — a literal rubber duck. Explain what your program is supposed to do, step by step, out loud.
Often, partway through explaining, you’ll catch the bug yourself. Saying it out loud forces your brain to think about it differently.
Programmers do this so often there’s a name for it: rubber-duck debugging. The duck doesn’t have to understand. You’re not explaining it to the duck — you’re explaining it to yourself, with the duck listening.
Step 5: Look at how you did this before
If you’ve done something similar in a past session and it worked, look at how you did it before. Open the textbook chapter, look at the code. Often, your current problem is the same shape as a problem you already solved.
This is one of the highest-leverage moves in programming. Real programmers do this all day long — they look at their old code, or a coworker’s old code, and adapt it to the current problem. Copying-and-modifying is fine. Copying without understanding is not.
(That’s the explain-how-it-works rule — if you copy something, you have to be able to explain it. Otherwise, you’re not done.)
Step 6: Ask your buddy
If you’ve worked through steps 1-5 and you’re still stuck, ask your buddy.
Explain to them what you’re trying to do, what you’ve tried, and what’s not working. They might see something you missed. Or they might be stuck on the same kind of problem and you can figure it out together.
Step 7: Ask Mr. Eric
Your buddy is your first line of help. Mr. Eric is your second.
When you ask Mr. Eric, tell him what you’ve already tried. This isn’t a rule for his benefit — it’s a rule for yours. If you can say “I tried X and Y and Z and none of them worked,” you’ve already done most of the thinking work. Often, telling him what you tried is when you figure it out.
Step 8: Take a break
If you’ve been stuck for more than about 20 minutes, your brain is tired. Get some water, walk around for two minutes, look out a window. Then come back.
This is not goofing off. This is one of the most-used techniques of professional programmers. Brains find solutions when you stop pushing them. Try it.
What stuck isn’t
A few things that feel like being stuck but aren’t:
- Frustration. Frustration is normal but it isn’t a clue. The bug doesn’t care that you’re frustrated. Take a breath, go back to step 1.
- “It’s broken.” “It’s broken” isn’t useful information. What exactly is broken? What did you expect it to do? What did it actually do? Force yourself to be specific. Half the time, forcing yourself to be specific is when you find the bug.
- “I’m not smart enough for this.” No. You just haven’t found it yet. Every programmer who has ever lived has felt this. They kept going. The feeling passes.
The big idea
Getting unstuck is a skill, not a talent. You get better at it the same way you get better at anything: by doing it, over and over, until your brain learns the moves.
Every time you fix a bug, you’re a slightly better programmer than you were before. Every time. That’s the whole job.
Reading error messages
Coming soon.
This appendix will cover how to read the error messages Python shows you when your program doesn’t work. Most error messages are trying to help. They tell you which file, which line, what the problem is, and sometimes what to do about it.
We’ll add this once the class hits Phase 3 (Python basics) and real error messages start showing up. Until then, the short rule is: read the bottom line of the error first. That’s almost always the most important part.
Glossary
Programming words, in plain language. New words get added as they come up in chapters. If a word here doesn’t make sense yet, don’t worry — it’ll show up in a session and click into place.
Words from later phases (web, etc.) get added when those phases start. So far this glossary covers Phase 1 (Scratch) and Phase 2 (Python with Turtle).
Phase 1 — Scratch words
Block
The colorful pieces you snap together in Scratch. Each block is
one instruction (move 10 steps, wait 1 seconds, etc.). Blocks
have shapes that fit together — that’s how Scratch tells you what
goes where.
Conditional
A programming idea: “if something, then do this.” In Scratch, the
if block. The “if” part is a question Scratch can answer with
yes or no. If the answer is yes, the inside runs. If no, it
doesn’t.
Costume A different look for a sprite. Many sprites have multiple costumes built in (like a cat with two different walking poses). Switching costumes is how sprites animate.
Event A thing that happens while a program is running — a key being pressed, a sprite being clicked, the green flag being clicked. Each event can trigger a script to run. Sprites can have many scripts, each listening for a different event.
Extension An extra category of blocks you can add to Scratch. The most useful one for Phase 1 is Pen, which lets sprites draw lines as they move. Add an extension by clicking the “Add Extension” button at the bottom-left of the Scratch window.
Forever A loop that runs its inside over and over until you click the red stop sign. Used a lot in games — “every fraction of a second, check the keyboard and update the cat’s position.”
If block The Scratch block that does conditional logic. Has a diamond- shaped slot at the top (for the question) and a mouth (for what to do if the answer is yes).
Loop
A way to do something multiple times without writing it multiple
times. Scratch has repeat N (do this N times) and forever (do
this until I stop you).
Mouth
The opening in some Scratch blocks (like repeat, forever, and
if) where other blocks go inside. Things in the mouth run as
part of the outer block.
Pen
A Scratch extension that lets sprites draw as they move. pen down starts drawing; pen up stops. Used in Session 3 to draw
shapes.
Script
A stack of blocks that runs together, starting from the top
(usually a “starter” block like when green flag clicked or
when [key] pressed). One sprite can have many scripts; each one
runs when its starter block triggers.
Sensing
A category of Scratch blocks that check things in the world —
“is the space key pressed?”, “am I touching another sprite?”,
“what’s the mouse pointer’s x position?” Sensing blocks usually go
inside if blocks.
Sequence A series of blocks that run one after another, top to bottom. The most basic kind of program. Order matters.
Sprite A character or object on the Scratch stage. The default sprite is the orange cat. Sprites have their own scripts, costumes, and position on the stage.
Stage The big rectangle in Scratch where sprites do their thing. The stage has a coordinate system: x goes left/right, y goes up/down, and (0, 0) is the middle. The stage is about 480 wide × 360 tall.
Variable
A labeled box that holds a value (usually a number). You can put
something in (set), change what’s in it (change), and read it
back ((varname)). Used for things that change over time, like a
score. Created with the “Make a Variable” button in the Variables
category.
X position / Y position Where a sprite is on the stage. X is left-right (negative is left, positive is right). Y is up-down (negative is down, positive is up). The middle of the stage is x = 0, y = 0.
Phase 2 — Python words
Argument
A value you pass to a function when you call it. In
t.forward(100), the 100 is an argument — it’s the
information the function uses. (See also: parameter.)
Comment
Text starting with # that Python ignores. Used to leave notes
in your code for yourself or other readers — like a label on a
section of code. Comments don’t change what the program does.
Elif
Short for “else if.” A way to check another condition after an
if’s condition was false. You can chain as many elifs as you
want before a final else (or no else at all).
Else
The “otherwise” part of an if. The body under else runs when
the if’s condition is false. Optional — you don’t always need
one.
Error message
What Python tells you in red text when something goes wrong.
Error messages are trying to help. They usually tell you the
file, the line number, and what kind of problem (SyntaxError,
NameError, AttributeError, etc.). Read them slowly.
Float
A number with a decimal point, like 5.0 or 3.14. Division
in Python (/) always gives a float, even when the answer is
whole (15 / 3 is 5.0, not 5).
For loop
The Python version of Scratch’s repeat. for i in range(N):
runs the indented body N times. The i is a loop variable that
takes the values 0, 1, 2, …, N-1.
Function
A named chunk of code you can call by name. You define it with
def name(): and the indented body underneath. You call it by
writing name() somewhere later. The biggest new idea in
Phase 2.
IDE Short for Integrated Development Environment. A program for writing code. Thonny is an IDE. So are VS Code, PyCharm, and many others. We use Thonny in this class.
Import
A Python statement that brings in a module (a toolbox of code
written by someone else) so you can use it. import turtle
brings in the turtle module so you can do t = turtle.Turtle().
Indentation
Spaces at the start of a line. In Python, indentation is part
of the grammar — it’s how Python knows which lines belong inside
a for loop, if, def, etc. Lines indented underneath are
“inside”; lines not indented are “outside.” Standard is 4
spaces.
Module
A bundle of Python code you can import and use. Comes from
Python’s standard library (like turtle) or from the internet
(like Pygame, later in the class). Sometimes also called a
library.
Operator
A symbol that does something. Math operators: + - * /.
Comparison operators: < > == != <= >=. The = sign is the
assignment operator (gives a variable a value).
Parameter
A placeholder name in a function definition. In
def draw_square(size):, size is the parameter. When you call
the function with draw_square(50), the parameter size gets
the value 50 for that call. (See also: argument.)
Print
A Python function (print(...)) that displays text or values in
the shell. Useful for showing output to the user, and especially
useful for debugging — print(size) shows what size is right
now.
Python The programming language we use after Scratch. Real, general-purpose, used by professional developers, scientists, data analysts, web developers — almost everyone. We use it through Thonny.
range
A built-in Python function that gives a sequence of numbers.
range(5) is the numbers 0, 1, 2, 3, 4. Almost always used
inside a for loop: for i in range(5): runs five times.
Run To execute your program. In Thonny, click the green Run button or press F5. Python reads your code top to bottom and does what it says.
Save
To write your code to a file on your computer. Files end in
.py. Save before you Run, or Thonny will sometimes complain.
Use Ctrl-S as a shortcut.
Shell The bottom panel of Thonny. Where Python prints messages, where errors appear, and where you can type one-line Python experiments interactively (without saving a file).
SyntaxError A Python error that means “your code’s grammar is wrong.” Usually a missing colon, missing parenthesis, or wrong indentation. Python points at where it noticed the problem (which is sometimes a line after the actual mistake).
Thonny The Python editor we use in class. Has an editor (top), a shell (bottom), and a green Run button. Pre-installed on every class machine.
Turtle
The Python module we use in Phase 2 to draw on the screen.
Imported with import turtle. The turtle is a little arrow
(or actual turtle shape) that moves around drawing lines.
General programming words
Bug A mistake in code that makes it do the wrong thing. Named after a real moth that got stuck in an early computer in 1947 and stopped it from working.
Code A general word for “instructions you wrote for the computer.” Scratch blocks are code. Python lines are code. HTML tags are code.
Debugging Fixing bugs. Half of programming.
Program A complete set of instructions that does something. Your Scratch projects are programs. So is every app on your phone.
Programmer A person who writes programs. Also: you, after about three weeks of class.
This glossary will grow as the curriculum moves into Python and beyond. If a word should be here and isn’t, ask Mr. Eric to add it.
Keyboard shortcuts
Coming soon.
A quick reference for the keyboard shortcuts you’ll use most often in class — for the operating system (Linux Mint XFCE), for Thonny (the Python editor), and for the web browser. Knowing a few shortcuts saves a surprising amount of time over a year.
Thonny reference
Coming soon.
Thonny is the Python editor we use in class. This appendix will be a quick reference for the parts of Thonny you’ll use most: running your program, using the debugger, finding the variables view, and the few menu items that actually matter day-to-day.
Relevant once we get to Phase 3 (Python basics).
Installing on your own computer
If you’re in the class at Old Zion Hill, the computer provided to you already has everything you need. You don’t have to install anything.
But maybe you want to:
- Practice at home on a family laptop.
- Keep working on your projects when school’s out and your class computer isn’t with you.
- Show a friend how Scratch works.
- Eventually use Python on your own machine, on Linux, on a Chromebook with Linux enabled, or on something else entirely.
This appendix tells you how. It’s organized by phase — start at the section that matches where you are in the curriculum.
Phase 1: Scratch
Scratch has two ways you can use it:
Option 1: In a web browser (easiest)
Go to scratch.mit.edu in any modern browser (Chrome, Firefox, Safari, Edge). Click “Create” at the top. You’re in.
That’s it. No install. Works on Windows, Mac, Linux, Chromebook — anything with a browser and internet.
You can save projects to your computer (File → Save to your computer) or, if you make a free account, to the Scratch website.
Limits:
- Needs internet to start (once it’s loaded, you can usually keep working without internet, but saving requires reconnection).
- Can be slow on really old computers.
Option 2: Scratch Desktop (works offline)
Download from scratch.mit.edu/download. Available for Windows and macOS (and through other channels for Linux).
Once installed, you can use Scratch with no internet. Your projects save to files on your own computer.
Limits:
- Doesn’t auto-sync with the website.
- Updates come a little slower than the web version.
Which to pick?
If you’re not sure, use the web version. It’s the same Scratch either way. You can switch later.
Phase 2 onward: Python
Coming with Phase 2.
When the class moves to Python (Phase 2), you’ll need to install Thonny, the Python editor we use in class. The short answer: download it from thonny.org for any computer (Windows, Mac, Linux). Thonny includes its own copy of Python, so you don’t have to install Python separately.
Detailed instructions for getting Thonny set up — including how to verify it’s working, what to do if it’s already installed, and how to handle the Linux Mint version that ships preinstalled — will be added here when Phase 2 begins.
Phases 5 onward: customtkinter, Pygame, Flask
Coming with each phase as it begins.
Each later phase introduces a Python library you’ll need to install
on top of Thonny. Instructions will be added to this appendix as
each phase starts. The general pattern is the same: open a
terminal, type pip install <library-name>, and you’re done.
For now: don’t worry about it. The class machines have everything preinstalled. This appendix grows as you grow.
A note about your class machine
The computer you got from the class is yours. You keep it. You can take it home, use it for whatever, install whatever you want on it. It already has Scratch, Thonny, and (eventually) every Python library the curriculum uses. You don’t need to install anything on it.
The instructions on this page are for other computers — a family laptop, a friend’s computer, a school Chromebook, whatever. The class computer is already set up.