Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. What we’re learning today. A short paragraph or two on the point of the session.
  2. You’ll need to remember from last time. A few bullet points from previous sessions you’ll lean on today.
  3. Part A. A new concept, with a guided exercise.
  4. Part B. Practice or a small project that uses Part A.
  5. Wrap-up. A few questions to think about.
  6. If you missed this session. The catch-up version.
  7. Stretch and extension ideas. Optional extras.
  8. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

SessionIdeaWhat it lets you do
1Welcome & the peanut butter problemUnderstand why precision matters in programming
2SequencesTell sprites to do things, in order
3LoopsMake sprites do something many times without writing it many times
4EventsControl sprites in real time with the keyboard and mouse
5ConditionalsLet your program decide what to do based on what’s happening
6VariablesMake your program remember things, like a score
7Putting it togetherCombine everything into one small game
8-9Milestone projectPlan, 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.

  1. Drag a move 10 steps block (it’s blue, near the top of the blocks panel) into the script area in the middle.
  2. Click on the block. The cat moves a little.
  3. Click on it again. It moves more.
  4. Drag a turn 15 degrees block onto the bottom of the first one. They should snap together.
  5. Click on the stack. The cat moves and turns.
  6. 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:

  1. 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.
  2. Open Scratch on your computer. It’s already installed — look for the orange cat icon on your desktop or in the Applications menu.
  3. 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 say block (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: 0 block (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?

  1. Start at the left side of the stage.
  2. Walk to the middle.
  3. Say “Hello!” for two seconds.
  4. Wait a moment.
  5. 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 steps blocks, or one glide 2 secs to x: 0 y: 0 block (also Motion). The glide version 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 clicked block, 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:

  1. Drag a when green flag clicked block (yellow, Events) into the script area.
  2. Snap a move 50 steps block underneath it.
  3. Snap a wait 1 seconds block underneath that.
  4. Snap a turn 90 degrees block underneath that.
  5. Snap one more move 50 steps block underneath.
  6. Click the green flag. Watch the cat.
  7. Now drag the wait block 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 glide blocks instead of move blocks 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, and go 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 repeat to 3 and the turn to 120 degrees. What shape do you get? Now try 5 and 72. Now try 6 and 60. 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:
    repeat [12]
       repeat [4]
          move 50 steps
          turn ↻ 90 degrees
       turn ↻ 30 degrees
    
    Click the green flag. You should get a beautiful flower-of-squares pattern.

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:

  1. Add a when green flag clicked block.
  2. From the Control category, drag out a repeat 10 block. Snap a move 10 steps block inside its mouth. Snap the whole thing under the green flag. Click the green flag. The cat moves ten times in a row.
  3. Change the inside to move 50 steps and turn 90 degrees, and change the repeat number to 4. Click the green flag. The cat walks a square.
  4. Click the Add Extension button (bottom-left). Pick Pen.
  5. Add erase all, go to x: 0 y: 0, and pen down to 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 to to make the lines thicker.
  • Make a spiral by combining move and turn outside a loop:
    repeat [50]
       move 5 steps
       turn ↻ 10 degrees
    
    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?
  • Try using a forever block instead of repeat. (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 repeat loop — 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 pressedchange y by 10
  • when [a] key pressedchange x by -10
  • when [s] key pressedchange y by -10
  • when [d] key pressedchange 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:

  1. Look at the Events category (yellow blocks). Drag out a when [space] key pressed block.
  2. Snap a say [Hi!] for 1 seconds block under it. Press the spacebar to test.
  3. Drag out a when this sprite clicked block. Snap a turn 15 degrees block under it. Click the cat to test.
  4. Now build the four arrow-key movement scripts described in Part A above (right, left, up, down). About 10 minutes.
  5. 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 pressed to 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

  • Eventswhen [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: -130 inside the when green flag clicked script (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 arrow and down arrow if blocks 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 200 is in the Operators category (the green blocks). It’s another diamond-shaped block, but it goes into the x slot of go to because that slot accepts numbers. It picks a random x-coordinate each time, so the apple appears somewhere different.
  • Sprite1 is 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:

  1. Build the smooth arrow-key cat from Part A (one forever loop, four if blocks). About 10 minutes.
  2. Add an apple sprite (or any sprite that can “fall”).
  3. Build the apple’s falling-and-resetting script described in Part B above. About 15 minutes.
  4. 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 — like if, but with two mouths). Lets you do “if X then this, otherwise that” in one block. More elegant than two if blocks 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 seconds block 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 (if blocks) 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 clicks variable.
  • 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:

  1. Click Make a Variable in the Variables category. Name it clicks. Click OK.
  2. Build the two scripts from Part A: green-flag-resets-to-0, and sprite-clicked-changes-by-1. Click the cat to test.
  3. 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).
  4. Add a score variable. Reset it to 0 on green flag. Have the apple’s “if touching cat” block also change 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 set vs change distinction. Try set score to 10 and 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

  • Variablesset, change, (read), displayed on stage.
  • The if block + 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 projectyour 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:

  1. Build the cat with smooth left/right movement at the bottom of the stage. Reset score to 0 on green flag. (Part A above.)
  2. Add an apple that falls, gives a point on catch, resets either way.
  3. Add a rock that falls. If it touches the cat: say "Game Over" for 2 seconds, then stop [all]. If it falls off the bottom: reset to top.
  4. 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.

  1. What’s the name of your game? (Make one up. You can change it later. Examples: “Donut Drop,” “Cat vs. Asteroids,” “The Royal Quest.”)

  2. 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.”)

  3. What sprites do you need? (List them. Example: “Wizard sprite, three book sprites, a tower-base backdrop.”)

  4. What does the player do? (How do they control things? Arrow keys? Click sprites? WASD? Some combination?)

  5. What’s the goal? (How does the player know they did well? Score that goes up? Reach a place? Survive a long time?)

  6. What can go wrong? (How does the game end, or get harder? Game over? Missed too many? Time runs out?)

  7. 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:

  1. Wizard sprite that moves with arrow keys.
  2. One book sprite that falls.
  3. 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:

  1. Read your code carefully. Did you tell the computer the exact thing you meant? (Remember the peanut butter problem.)
  2. Ask your buddy. They might see something you don’t.
  3. Look at how you did something similar before. If the apple in Apples and Rocks worked the way you want, copy that pattern.
  4. 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:

  1. Spend 10-15 minutes answering the seven planning questions above on paper or in a text file.
  2. Start building the simplest version of your game (look at your answer to question 7).
  3. 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:

  1. It runs. You can click the green flag and play it without crashing or getting confused.
  2. It does what your plan said it would do — at least the base version (your answer to question 7 from Session 8).
  3. 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:

  1. 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.)
  2. Tell us about it. What’s it called? What were you trying to make? What’s the goal of the game?
  3. 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.
  4. 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

SessionIdeaWhat’s new
1Welcome to Python (typing, not dragging)Open Thonny, your first line of Python, your first error message
2Telling the turtle what to doMore turtle commands; coordinate-based positioning
3Loops in Pythonfor i in range(N): — the Python version of repeat
4FunctionsThe big new idea: making your own commands
5Variables and a little mathx = 5 syntax; arithmetic in code
6Conditionals: if in Pythonif x > 5: — the Python version of the if block
7Putting it together — a drawing projectCombine everything into a creative pattern
8Milestone project + demoPlan, 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 name t. (You can name it anything you want, but t is short and quick to type. Some people name it bob. Some name it george. The turtle doesn’t care.)
  • t.forward(100) — this tells the turtle named t to 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:

ScratchPython
Drag a move 10 steps blockType t.forward(100)
The cat movesThe 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 typed forwerd. 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 drawing
  • t.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() and pendown() 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 of t.right(90) and see what happens.
  • Try t.backward(50) to walk the turtle backward.
  • Try t.shape("turtle") (right after the t = ... line). The arrow becomes an actual little turtle shape. Then try "square", "circle", "triangle", "arrow".
  • Try t.speed(1) (slow) or t.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 .py extension, click Run (or press F5).
  • Every Python turtle program starts with import turtle and t = turtle.Turtle().
  • t.forward(N), t.right(N), t.left(N) for movement.
  • t.color("red") and t.pensize(N) for style.
  • t.penup() and t.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 goto to position the turtle at one corner, setheading to face the right way, then three forward/left pairs 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 circle
  • t.shape("turtle") — make the cursor look like a little turtle
  • t.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:

  1. Build the standard opening (import turtle and t = turtle.Turtle()).
  2. Practice with goto: t.penup(), t.goto(100, 50), t.pendown(), t.forward(50). Run and see the turtle jump.
  3. Practice with setheading: t.setheading(90), t.forward(50). The turtle now draws a line going up.
  4. Practice with circle: t.color("yellow"), t.circle(40). The turtle draws a yellow circle.
  5. 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 with t.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, do t = turtle.Turtle() and s = 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 turtle and t = 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.” The range(4) produces a sequence of numbers from 0 to 3 (which is four numbers — Python starts counting at 0, not 1).
  • i is the loop variable. Each time through the loop, i takes the next value from the sequence (0, then 1, then 2, then 3). We don’t have to use i in this loop — but the syntax requires we name it. Most programmers use i by default.
  • The colon at the end of the for line is required. Don’t forget it.
  • The lines underneath the for are 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) and t.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:

  1. Build a for loop 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.

  2. Change range(4) and t.right(90) to draw a triangle (3 sides, 120 degrees) and a pentagon (5 sides, 72 degrees).

  3. 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 / goto between 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 for are 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 a for loop.
  • 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 parameter size goes in the parentheses.
  • t.forward(size) — instead of always 60, we use whatever size was 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() and draw_triangle() — small building blocks
  • draw_house() — uses the small ones
  • draw_neighborhood() — uses draw_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() and draw_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:

  1. 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.

  2. Modify it to take a size parameter, and call it multiple times with different sizes (with penup/goto between).

  3. 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 either draw_square() (uses 50) or draw_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): using range(sides) and 360 / sides as 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

  • Functionsdef name(parameter): to define, name(value) to call. The body is indented underneath.
  • Loopsfor 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 0 and change 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:

OperatorMeansExample
+add5 + 3 is 8
-subtract5 - 3 is 2
*multiply5 * 3 is 15
/divide15 / 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:

  1. Make a variable: size = 50. Build a square that uses t.forward(size) and run it. Change size to 100, run again. Same code, different square.

  2. Practice changing variables: size = size + 10. Try printing it after each change with print(size).

  3. Build the tower from Part A — a for loop that draws a square of size size each iteration, then increases size by 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 — x and y for position, size for 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): then t.forward(i * 5) makes a spiral that grows linearly. Combine with t.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

  • Variablessize = 50, size = size + 10.
  • Math operators+, -, *, /.
  • Functions with parametersdef draw_square(size):.
  • The if block 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. Is size greater than 50? Yes or no?
  • : — the colon (required, just like for and def).
  • 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:

OperatorMeansExample
>greater than5 > 3 is True
<less than5 < 3 is False
>=greater than or equal to5 >= 5 is True
<=less than or equal to5 <= 4 is False
==equal to5 == 5 is True
!=not equal to5 != 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 and or or? (Hint: those let you combine multiple conditions, like if 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:

  1. Build the basic if/else:

    size = 30
    
    if size > 50:
        print("Big!")
    else:
        print("Small!")
    

    Run. Then change size = 70 and run again. Different output.

  2. Try elif with three categories — small, medium, large.

  3. 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 as if size <= 50:. Sometimes one reads better than the other.
  • Conditional inside a loop, deciding what to draw each iteration:
    for i in range(8):
        if i < 4:
            # draw a triangle
            pass
        else:
            # draw a square
            pass
    
    Make a row that’s half triangles, half squares.

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 parametersdef name(parameter):
  • Variables and arithmeticsize = 50, size = size + 10
  • For loopsfor i in range(N):
  • Conditionalsif/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_flower takes 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:

  1. Build the draw_flower(x, color, petal_size) function from Part A. Call it once to draw a single flower.

  2. Call it three or four more times with different positions, colors, and sizes.

  3. Wrap the calls in a for loop driven by an x variable that starts at -200 and increases by 100 each iteration.

  4. 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/else color-picker with a list. (We haven’t formally taught lists, but a curious student can try: colors = ["red", "orange", "yellow", "purple", "pink"] and then color = 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:

  1. What are you drawing? (One sentence. Examples: “A snowy mountain landscape.” “My initials in giant decorated letters.” “A snowflake pattern.”)

  2. What functions do you need? (Each function does one thing. Examples: draw_mountain(), draw_tree(), draw_letter_A(), draw_petal(). Brainstorm 2-5.)

  3. What will vary? (What are the parameters? Position? Color? Size? List which functions need which parameters.)

  4. 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:

  1. Read your code carefully. Did you tell the computer the right thing?
  2. Read the error message. Most Python errors point at the exact line.
  3. Look at how you did something similar before. Last week’s garden? Open it for reference.
  4. Ask your buddy.
  5. 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:

  1. It runs. You can press the green flag and see something without an error.
  2. It does what your plan said it would do — at least the simplest version (your answer to question 4).
  3. 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:

  1. Show your drawing. Run it. Let the class see it appear.
  2. Tell us about it. What were you trying to make? What functions did you write?
  3. 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.
  4. 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

SessionIdeaWhat’s new
1Welcome to Python without turtleprint, input, the shell
2Variables and typesint, float, string, type conversion
3Stringsconcatenation, methods, f-strings, indexing
4Conditionals (deeper)and, or, not; multi-condition logic
5Loops + the Thonny debuggerwhile loops; the debugger formally
6Functions + return valuesreturn; thinking about scope
7First real project — number-guessing gameFirst text-based game
8Liststhe most important data structure in Python
9More lists; iteration patternsfor over lists, len, append, slicing
10Dictionarieskey-value lookups
11Reading filesopen, line iteration
12Hangmanbigger project applying lots
13Error handlingtry / except
14Putting it together — text adventure or CSV readerBig integration project
15Milestone project work day 1Plan + build
16Milestone project work day 2 + demoPolish + 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 called name.
  • 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 called name.
  • 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:

  1. Type print("Hello, world!"). Save. Run. Look at the shell at the bottom — your message appears.

  2. Add a few more print lines with different text or simple math.

  3. Now try input:

    name = input("What's your name? ")
    print("Hi, " + name + "!")
    

    Save. Run. Type your name when prompted.

  4. 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.
  • print with 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).
  • Variablesname = "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:

TypeWhat it isExamples
inta whole number (integer)5, 42, -3, 0
floata decimal number3.14, 5.0, -0.5
strtext (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 + 3 is 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:

FunctionConverts toExample
int(x)integerint("5") is 5
float(x)decimalfloat("3.14") is 3.14
str(x)stringstr(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 + years adds 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 TypeError they 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:

  1. Try print(type(5)), print(type(5.0)), print(type("hi")). See the three types: int, float, str.

  2. Try print(5 + 3) and print("5" + "3"). Notice the different answers.

  3. 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) + ".")
    
  4. 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 does int(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 conversionint(...), 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:

  1. Put f in front of the opening quote.
  2. Put variables in {curly braces}.
  3. 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:

MethodWhat 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-insensitive
  • for letter in name: — a loop that goes through each letter of the string
  • if 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:

  1. 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}.")
    
  2. Try .upper(), .lower(), len() on a string. Print the results.

  3. Try indexing: name[0], name[-1], name[1:3].

  4. 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") and s.endswith("!") return True/False. Useful in conditionals.
  • s.find("substring") returns the position where a substring first appears (or -1 if 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-stringsf"Hello, {name}!".
  • Comparison operators>, <, ==, !=, >=, <=.
  • if/elif/else from 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:

WordWhat it meansExample
andboth must be trueage >= 13 and height >= 50
orat least one must be truename == "Sam" or name == "Alex"
notthe oppositenot 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 >= 13 saves the result of the comparison (a True or False value) in a variable. This is called a boolean — a value that’s either True or False.
  • The if statements 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:

  1. Try basic and, or, not examples:

    age = 15
    if age >= 13 and age < 20:
        print("Teenager.")
    
  2. 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”
  3. 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 or and and matters when they’re mixed. True or True and False is True because and is evaluated first. Use parentheses when in doubt: (True or True) and False is False.
  • True and False (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

  • for loopsfor i in range(N): runs N times.
  • Combined conditionalsand, or, not.
  • BooleansTrue and False.
  • Indentation — code inside a loop or if is 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 while check: is 2 <= 5? Yes. Run again.
  • …continue until count is 6. Now 6 <= 5 is 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:

  1. Click the bug icon to start debugging.
  2. Watch the variables panel.
  3. Click “Step Over” to advance one line at a time.
  4. 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 while loop made more sense than a for loop?
  • 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:

  1. Build a while loop that counts up:

    count = 1
    while count <= 5:
        print(f"Count is {count}")
        count = count + 1
    

    Save, run.

  2. Build the guess-the-number version (Part A above).

  3. 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 where vowel_count = 1 is wrong (should be vowel_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) — like break, but skips the rest of the current iteration and moves to the next one. Less useful than break; mention if asked.
  • while with multiple exit conditionswhile not done and not error:. Combine booleans with while.

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

  • while loopswhile condition: runs as long as the condition is true.
  • The Thonny debugger — bug icon button, Step Over (F6).
  • Functionsdef 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 calculates x * 2 and returns it to the caller.
  • result = double(5) — call the function with 5; whatever the function returns gets put into result.
  • 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 numbers
  • letter_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 into avg.
  • grade = letter_grade(avg) — the letter_grade function’s return value goes into grade. It uses avg, 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 secret wasn’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:

  1. Build a double function:

    def double(x):
        return x * 2
    
    print(double(5))    # 10
    
  2. Build an add function with two parameters that returns their sum.

  3. Build the grade calculator from Part B (average and letter_grade functions).

  4. 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 with n, 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 return returns None automatically. Try def nothing(): pass then print(nothing()). You’ll see None.

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

  • print and input for output and asking the user.
  • int(input(...)) to get a number from the user.
  • if/elif/else for branching.
  • while loops for “until something is true.”
  • break to 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:

  1. The computer picks a secret number between 1 and 100.
  2. The user guesses a number.
  3. If the guess is too low, say so. If too high, say so. If correct, celebrate and stop.
  4. 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:

  1. import random and secret = random.randint(1, 100). Print secret while testing.
  2. Ask for a guess with int(input(...)).
  3. Compare with if/elif/else. Print “too high” / “too low” / “correct.”
  4. Wrap in a while True: loop with a break on the correct guess.
  5. 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).
  • for loopsfor i in range(N):.
  • while True: and break for “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 break out of the loop.
  • Otherwise, we .append() what they typed to the list.
  • After the loop ends, len(favorites) and a for loop 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:

  1. Make a list and try basic operations:

    colors = ["red", "blue", "green"]
    print(colors[0])     # red
    print(len(colors))   # 3
    colors.append("yellow")
    print(colors)
    
  2. Loop through it:

    for color in colors:
        print(f"I like {color}.")
    
  3. 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.
  • in for membership — "red" in colors returns True/False. Same in you used for if 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"] * 5 is ["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().
  • Loopingfor 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:

MethodWhat 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 with break on “done” — same pattern as the favorites collector.
  • int(entry) converts the input string to a number before appending. Without this, the max/min/sum functions 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, len for the statistics.
  • :.1f in 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 to scores.sort() which sorts in place.)
  • sorted_scores[:3] slices the first three.
  • enumerate numbers 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, and sum shortened 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:

  1. Try slicing on a list:

    numbers = [10, 20, 30, 40, 50]
    print(numbers[0:3])
    print(numbers[::-1])
    
  2. Use enumerate:

    colors = ["red", "blue", "green"]
    for i, color in enumerate(colors):
        print(f"{i}: {color}")
    
  3. Try max, min, sum:

    scores = [85, 92, 78, 95]
    print(max(scores))
    print(sum(scores) / len(scores))
    
  4. 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’s statistics module has statistics.mean(scores)import statistics and 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 with grid[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.
  • enumerate for looping with index.
  • max, min, sum for stats.
  • while True: and break for “until done” loops.
  • The in operator"red" in colors returns 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] = phone adds a new pair (or updates an existing one).
  • Look up: if name in contacts: checks for the key first (avoids KeyError); then contacts[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:

  1. Make a dictionary and look up values:

    person = {"name": "Sam", "age": 12}
    print(person["name"])
    print(person["age"])
    
  2. Add and update keys:

    person["color"] = "blue"
    person["age"] = 13   # update
    print(person)
    
  3. Iterate:

    for key, value in person.items():
        print(f"{key}: {value}")
    
  4. 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) — like dict[key] but returns default if 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.
  • for loops for iterating through collections.
  • while True: and break for 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 name f inside the block.
  • f.write("Hello, file!\n") — writes a string to the file. The \n at the end is a newline (line break), so the next thing written goes on a new line.
  • The with block automatically closes the file when it ends. That’s the magic of with — you don’t have to remember to clean up.

Three modes

The second argument to open() is the mode:

ModeWhat 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:

  1. Write a file:

    with open("hello.txt", "w") as f:
        f.write("Hello, file!\n")
    

    Save, run. Look for hello.txt in the same folder.

  2. Read it back:

    with open("hello.txt", "r") as f:
        for line in f:
            print(line.strip())
    
  3. 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 \n still attached to each — usually you .strip() after).
  • CSV files — comma-separated values, the format of spreadsheets. Python has a csv module. 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: and break for game loops.
  • Functions with return values — Session 6.
  • import random and random.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:

  1. The computer picks a secret word.
  2. The user sees the word with _ for letters they haven’t guessed.
  3. The user guesses one letter at a time.
  4. If the letter is in the word, fill in the blanks.
  5. If not, count it as a wrong guess.
  6. 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 display string 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 = 0 and max_wrong = 6 — track how many wrong guesses, with a limit.
  • The while True: loop with break for 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; continue skips 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:

  1. Build the make_display(secret, guessed_letters) function from Part A. Test it with a few inputs.

  2. Add the game loop from Part B (Step 4) — while True:, win check, lose check, get guess, track wrong count.

  3. Play it. Get the hardcoded version working before trying to load words from a file.

  4. Optionally: create a words.txt file 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_wrong values.
  • 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 handlingtry/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: and break — 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 try runs as normal.
  • If an error happens, Python jumps to the except block matching the error type.
  • If no error, the except block 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:

ExceptionWhen it happens
ValueErrorbad value given to a function (int("hello"))
ZeroDivisionErrordividing by zero
FileNotFoundErrortrying to open a file that doesn’t exist
KeyErrordict lookup with a missing key
IndexErrorlist lookup with an out-of-range index
TypeErrorwrong type passed ("5" + 3)
NameErrorusing 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 successful break out.
  • 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 patternwhile 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:

  1. 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.

  2. Build the try-again loop pattern from Part A.

  3. Build the ask_for_number function 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

  • else clause — try / except / else:. The else runs only if no exception happened. Useful for separating “the risky code” from “the code that depends on it succeeding.”
  • finally clause — try / except / finally:. The finally always runs, regardless of exceptions. Useful for cleanup.
  • Catching multiple exception types: except (ValueError, TypeError): — catches either.
  • Catching the exception itself: except ValueError as e:e is 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], the in operator, .items().
  • Lists — for inventories or anything ordered.
  • while True: and break for game loops.
  • if/elif/else for handling different commands.
  • f-strings for output.
  • try/except for 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.) or quit.
  • 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; update current.
  • 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:

  1. Build the rooms dictionary (Part A) — at least three rooms with exits.
  2. Build the game loop (the full Part A code).
  3. Run it. Navigate from entry to study (or wherever your win room is). Quit.
  4. 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 talk command).
  • 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(...) and json.load(...).)

What’s next

Next week starts your milestone projectyour 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:

  1. 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.”)

  2. What does the user do? (How do they interact? Type answers? Pick from a menu? Both?)

  3. What functions or data structures will you need? (Brainstorm 3-5. Examples: a dict of trivia questions, a function ask_question(q), a function save_journal(text), a list of items, etc.)

  4. 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?)

  5. What might go wrong, and how will you handle it? (Bad input? Missing file? Wrong type? Use try/except or if validation. 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:

  1. One hardcoded question.
  2. Ask it.
  3. 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. Add print(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:

  1. Spend 10-15 minutes answering the five planning questions above on paper or in a text file.

  2. Start building the simplest version of your project (look at your answer to question 4).

  3. 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/except so 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:

  1. It runs without crashing when you use it normally.
  2. It does what your plan said it would do — at least the simplest version (your answer to question 4 from last week).
  3. 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 (:.2f for 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:

  1. Show your program. Run it. Use it for a minute or two. Let the class see what it does.
  2. Tell us about it. What’s it called? What’s it for? What does it do?
  3. 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.
  4. 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, and python 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

SessionIdeaWhat’s new
1The command lineOpen a terminal; cd, ls, python script.py
2Multi-file programsSplitting code; import yourfile; modules
3Standard library modulesos, sys, datetime, pathlib
4A light intro to classesclass, __init__, methods
5Git — saving versionsinit, add, commit, log, diff
6Git in practicerecovering, undoing, light branching
7Testing your codeassert statements; simple test functions
8Milestone project work day 1plan + build (with Git from day one)
9Milestone project work day 2 + demofinish + 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 BankAccount with deposit/withdraw, or a Pet you 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 Documents then ls
  • cd Pictures then ls
  • cd .. to go back
  • cd ../Music to 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:

  1. No editor required. You can run a Python script anywhere, anytime, without opening Thonny.
  2. 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.
  3. It’s faster. Once you’re used to it, running a script with python myscript.py is 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:

CommandWhat it does
mkdir new_foldermake a new folder
rm filenamedelete a file (be careful — no undo)
cp source destinationcopy a file
mv source destinationmove (or rename) a file
cat filenameshow the contents of a text file
clearclear 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 mkdir or cat? 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:

  1. Type pwd and press Enter. See where you are.
  2. Type ls. See what’s in this folder.
  3. Type cd Documents (or wherever your .py files are). Then ls again.
  4. Type python yourfile.py to 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.
  • Pipescat words.txt | wc -l counts the lines in the file. The | pipes the output of one command into another. Powerful and confusing; mention if curious.
  • Output redirectionpython myscript.py > output.txt saves 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 terminalcd, 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.py
  • main.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.py can 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 function random_quote() that returns one
  • journal.py — has functions to read and write journal entries
  • main.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 every input() 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.py without touching main.py?
  • Did anyone get an ImportError or ModuleNotFoundError? 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:

  1. Create tools.py with a few simple functions like add and subtract.

  2. Create main.py that does import tools and uses tools.add(2, 3).

  3. Open a terminal, cd into your folder, run python main.py.

  4. 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:
    entries = [line.strip() for line in f if line.strip()]
    
    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 in journal.py and 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__.py file in the subfolder. Then import myfolder.mymodule.
  • Renaming on import. import really_long_module_name as mod — now you call it mod.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 world works exactly as shown. If you try this from a Windows home laptop and python isn’t found, try py tour.py hello world instead. Same behavior — sys.argv works 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:

  1. Takes the entry text from sys.argv.
  2. Uses datetime to figure out today’s date.
  3. Uses pathlib to build the filename.
  4. 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 a journals folder in the home directory.
  • .mkdir(exist_ok=True) — make the folder if it doesn’t exist. The exist_ok=True means “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 typed python 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.txt2026-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 delete command or a count command?

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:

  1. Try the datetime examples from Part A — print today’s date and the time.
  2. Try the pathlib examples — print your home directory.
  3. Try sys.argv — pass a few arguments when you run the script and print them.
  4. 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 def keyword for defining functions.
  • Indentation for grouping code.
  • Standard library classes like Path and datetime — 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 in my_pet.
  • my_pet.status() — call the status method on my_pet. Notice we don’t pass self — Python passes it automatically. The self inside the method refers to my_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:

  • Treat has a name and a food value (how much hunger it removes).
  • Pet has a numeric hunger (instead of just True/False).
  • Pet.eat() takes a Treat and 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:

  1. Type out the basic Pet class from Part A. Create one pet with Pet("Rex"). Call .status(), .feed(), .play(). Watch the state change.

  2. Try creating two pets. Feed one. Verify the other isn’t affected.

  3. 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 = 10 is shared by all pets. self.hunger is 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 what print(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 Dog class that’s a special kind of Pet. 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:

  1. Working tree — the actual files you’re editing.
  2. Staging area — files you’ve said “include this in the next snapshot.”
  3. 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:

  1. Make a change in Thonny.
  2. Save.
  3. git status to see what changed.
  4. git diff to see the actual differences.
  5. git add to stage.
  6. 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/else to 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 file
  • git commit -m "message" — save the snapshot
  • git 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:

  1. Run the one-time setup:

    $ git config --global user.name "Your Name"
    $ git config --global user.email "you@example.com"
    
  2. Create a folder, put a Python file in it.

  3. In the folder:

    $ git init
    $ git status
    $ git add yourfile.py
    $ git commit -m "Initial version"
    
  4. Make some changes, repeat steps 2-4 a few times.

  5. Run git log --oneline to 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 --graph shows commits in a visual tree.
  • git show <commit-id> shows the details of a specific commit (use the short ID from git log --oneline).
  • git diff --staged shows what’s about to be committed (already staged but not yet committed).
  • git commit -am "message" combines git add and git commit for already-tracked files (won’t add new files though).
  • Aliases — Git lets you create shortcuts. git config --global alias.s status makes git s mean git 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 cousin git 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 --hard is dangerous. It loses commits and any uncommitted work. Only use when you’re sure. For most situations, git revert is 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/except for 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:

  1. git restore undoes uncommitted changes (use carefully).
  2. git revert safely undoes a committed change (creates a new commit; doesn’t lose history).
  3. 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:

  1. Make a change to a file. Try git restore filename to throw it away.

  2. Make another change, git add it. Try git restore --staged filename to unstage.

  3. Make and commit a change you’ll want to undo. Try git revert HEAD to create an undoing commit.

  4. Make a branch with git checkout -b experiment. Make changes, commit. Switch back with git checkout main. Notice the file changes.

  5. Either merge with git merge experiment or delete with git 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.
  • .gitconfig aliasesgit config --global alias.co checkout makes git co mean git 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 testingassert 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 programstools.py and main.py imported 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:

  1. Make math_tools.py with a few simple functions (add, subtract, double).

  2. Make test_math_tools.py with assert statements testing each function.

  3. Run from the terminal: python test_math_tools.py. See the “All tests passed” message.

  4. Break one of your functions on purpose. Run the tests. See the failure. Fix it. Run again.

  5. 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 pytest then run with pytest. 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) — assert statements 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:

  1. What’s the program? (One sentence.)

  2. What does the user do? (How do they interact? Input? Subcommands? Menu?)

  3. 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.)

  4. Will you use classes? (Optional. If yes, what are they?)

  5. What’s the simplest version? (Build this FIRST.)

  6. What will you test? (At least one or two assert tests 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.argv for 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:

  1. Make the change.
  2. Test it (run the code, make sure it works).
  3. Commit (git add ...; git commit -m "...").
  4. 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:

  1. Spend 10-15 minutes answering the six planning questions.

  2. Create a folder for your project. git init in it. Create your first file. Commit.

  3. 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:

  1. It runs without crashing under normal use.
  2. It does what your plan said it would do — at least the simplest version.
  3. 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 status should say “working tree clean” before the demo.
  • Add a README — a README.md file 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:

  1. Show your project. Run it. Use it.
  2. Tell us about it. What is it? What does it do?
  3. 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?
  4. Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected.
  5. 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

SessionIdeaWhat’s new
1Welcome to GUIs — your first windowCTk(), CTkLabel, mainloop
2Buttons and eventsCTkButton + command callbacks
3Inputs — entries and text boxesCTkEntry, text input from the user
4Choices — checkboxes, radios, dropdownschoice widgets
5Layouts and framesgrid, CTkFrame for organized UIs
6Putting it together — a complete appguided build of a real app
7Milestone project work day 1plan + build
8Milestone project work day 2 + demofinish + 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.
  • importimport customtkinter as ctk brings 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 name ctk. We’ll use ctk. a lot today.
  • app = ctk.CTk() — create the main window (also called the “root window”). CTk is 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 inside app. CTkLabel is 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. pack is a layout method — it stacks widgets one after another. The pady=20 adds 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:

  1. 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.

  2. Add more labels with different fonts and sizes.

  3. Try ctk.set_appearance_mode("light") (before creating the window).

  4. 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 iconsapp.iconbitmap("icon.ico") (Windows) or with a .png for cross-platform. Need to find or create an icon file first.
  • pady and padx for 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 is command. 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 = 0 at the top — a regular variable that starts at 0.
  • def increment(): — the callback function.
  • global count inside the function — tells Python that when we say count, we mean the variable up top, not a new local one. Without global, Python would create a new local count and 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 count back 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:

  1. Build the basic button + callback from Part A. Click it; see the shell output.

  2. Build the counter app from Part B base. Notice how label.configure(text=...) updates the display.

  3. 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 buttonbutton.configure(state="disabled") grays it out. Re-enable with state="normal".
  • Hide a widgetlabel.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) runs my_function in 1000 milliseconds (1 second). Useful for animations and timers.
  • destroy() — close the window with app.destroy(). Call it inside a callback for a “Quit” button.
  • Lambda functionscommand=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

  • CTkButton with command=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. The placeholder_text is the faded text that shows when empty.
  • entry.get() — reads the current text in the entry. Returns a string.
  • The callback greet() calls entry.get(), then label.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=350 on 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:

  1. Build the basic greeter from Part A — entry + button + label that updates on click.

  2. Try entry.delete(0, "end") to clear after greeting.

  3. 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(): .... Use try/except ValueError for converting strings to integers.
  • bind to Enter keyentry.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

  • CTkButton with callbacks.
  • CTkEntry and entry.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() returns 1 if checked, 0 if 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_choice on 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.

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:

  1. Build the basic checkbox example from Part A. Check some boxes. Click submit. See the result.

  2. Build the radio button example. Notice how only one can be selected.

  3. Build the dropdown example.

  4. 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") and state="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=2 does 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() or grid() — 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 inside left_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() or grid() 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:

  1. Build the basic 2-column form from Part A using grid() instead of pack().

  2. Try columnspan to make a button span multiple columns.

  3. Build the two-section app with frames from Part B.

  4. 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 framesCTkFrame(app, border_width=2, border_color="gray") for visible borders.
  • Scrollable framesCTkScrollableFrame for 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() and CTkFrame — Session 5.
  • CTkEntry and entry.get() — Session 3.
  • CTkButton with 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: return ignores 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 of i as idx, then call delete_todo(idx) when the button is clicked. Without the idx=i part, all buttons would end up calling delete_todo with the last value of i. (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. No global. 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:

  1. Build the todo list step by step from Part A. Don’t skip steps — each one builds on the last.

  2. Test with a few todos. Add some, delete some.

  3. (Stretch) Add the JSON save/load from Part B. Verify that closing and reopening keeps your list.

  4. (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 CTkEntry that 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.
  • pack and grid for 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:

  1. What’s the app? (One sentence. “A flashcard quiz.” “A budget tracker.” “A drawing pad.”)

  2. What does the user do with it? (How do they interact? What do they click? What do they type?)

  3. What widgets will it have? (Labels, entries, buttons, lists, checkboxes? Sketch the window on paper if it helps.)

  4. What state does it track? (A list of items? A single value? A dictionary?)

  5. What’s the simplest version? (Build this FIRST. No persistence, no fancy features — just the core.)

  6. 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 padCTkCanvas (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 editorCTkTextbox, 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:

  1. Make the change.
  2. Run it. Make sure it works.
  3. Commit (git add ...; git commit -m "...").
  4. 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:

  1. Spend 10-15 minutes answering the six planning questions.

  2. Create a folder for your project. git init in it. Create your first file. Commit.

  3. 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") and ctk.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 grid weights 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:

  1. It runs without crashing under normal use.
  2. It does what your plan said it would do — at least the simplest version.
  3. 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") and ctk.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 READMEREADME.md in 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 status should 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:

  1. Show your app. Run it. Use it. Walk us through what it does.
  2. Tell us about it. What is it? Who is it for?
  3. Show your Git log. Run git log --oneline. Walk us through how the app came together — first commit, last commit, a turning point.
  4. 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.
  5. 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 .exe or .app that 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

SessionIdeaWhat’s new
1Welcome to Pygame — the frame loopThe 60-frames-per-second mental model
2Drawing on the screenpygame.draw, colors, shapes, the coordinate system
3Sprites and imagesLoading PNGs, blit, the Rect class
4Movement and the keyboardKey events + Clock for smooth motion
5Collision detectionRect.colliderect, simple physics
6Build Pong togetherYour first complete game
7GitHub — push to the worldgit push, repos as portfolios
8Sound and musicpygame.mixer, sound effects, background music
9Sprite classes and groupsThe Sprite class, organizing many things
10The grid-world (intro)Mr. Eric’s coding-puzzle game — play it
11The grid-world (extending)Build your own puzzles, add features
12Title screens and game overGame state, scenes
13Milestone day 1Plan + start your game
14Milestone day 2 + demoFinish + 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:

  1. Reads input (keys, mouse).
  2. Updates state (move things, check collisions).
  3. 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 filepython game.py from 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:

  1. Read input. What keys are pressed? What did the mouse just do?
  2. Update the world. Move characters, check collisions, advance the score.
  3. 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:

customtkinterPygame
Sits and waits for the userRuns constantly at 60 FPS
Calls a function when something happensReads input every frame
You don’t draw — widgets draw themselvesYou 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. The screen is 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:

  1. Move x by dx (which is 2, then maybe -2, then 2…).
  2. If x is off either edge, flip the direction (negate dx).

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, the while running loop, pygame.display.flip().
  • The Pygame coordinate system ((0, 0) top-left).
  • pygame.draw.rect and 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:

  1. Type out the minimum Pygame program. Run it. See the blank window.

  2. Add the orange rectangle. Run it. See the rectangle.

  3. Make it move (x = x + 2). Run it. See it slide off the screen.

  4. 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 dx and dy.
  • 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) and pygame.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) then surface.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) to clock.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. True means anti-aliased (smooth edges). The color is RGB.
  • screen.blit(text_surface, (50, 50)) — paste the rendered text at position (50, 50). blit is “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:

  1. Type the minimum Pygame program. Run it.

  2. Draw a rectangle, a circle, a line, and a polygon. Run after each to make sure it appears.

  3. 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 typespygame.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 a Surface (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.x and player_rect.y — top-left
  • player_rect.width and player_rect.height — size
  • player_rect.rightx + width
  • player_rect.bottomy + height
  • player_rect.center(cx, cy) of the rect
  • player_rect.centerx, player_rect.centery
  • player_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 propertiesx, 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:

  1. Type the minimum Pygame program. Add the image.load and blit lines to display your image.

  2. Switch to using a Rect (get_rect(), screen.blit(img, rect)).

  3. Move the sprite — try mouse.get_pos() to follow the mouse.

  4. 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 scale call).

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

  • Spritespygame.image.load, screen.blit, the Rect class.
  • 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.
  • Conditionalsif, 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 = 5 at 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 -= SPEED is shorthand for player_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 four if boundaries 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*40 is the Pythagorean-without-square-root version of “within 40 pixels.” Faster than calling math.sqrt.
  • f"Score: {score}" — f-string from Phase 3, drawn with font.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 constantspygame.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 properrect.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).

  1. Build the basic movable-player example from Part A first. WASD or arrows. Make sure boundary clamping works.

  2. Build the collector game from Part B. Adjust number of coins, position, and SPEED to taste.

  3. (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. Triggers make_fruit() every SPAWN_INTERVAL frames. 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 + random for procedural content.
  • Game statelives and score as 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).

  1. Build the basic collision example from Part A. Verify the background changes when you touch the coin.

  2. Build the fruit catcher from Part B. Adjust SPEED, SPAWN_INTERVAL, and starting lives to taste.

  3. (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 filewith 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.rect and circle (Session 2).
  • The Rect class (Session 3) — x, y, width, height, center, move_ip, clamp_ip.
  • get_pressed for 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, BLACK constants — 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:

  1. Build Pong step by step. Don’t skip steps. Run after each.

  2. By the end you should have two paddles, a bouncing ball, and a score.

  3. (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 using pygame.Surface alpha. 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.

  1. Go to github.com.
  2. Click Sign up.
  3. 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.)
  4. Use an email address you actually check. GitHub will send a verification email.
  5. Pick a strong password. Save it somewhere safe.
  6. 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

  1. After logging in, click the + in the top-right, then New repository.
  2. Repository name: pong. (Or whatever you called your project.)
  3. Description (optional): “My first Pygame project.”
  4. Public. (Private hides it from everyone. Public means anyone can see it. For a personal project, public is fine.)
  5. Do NOT check “Add a README file” or any other options. We want a completely empty repo.
  6. 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 my main branch to the origin remote, and remember that connection.” (The -u is short for --set-upstream. Future pushes can just be git 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

![Pong in action](screenshot.png)

The ![alt text](file) 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:

  1. Make a new repo on GitHub (empty).
  2. In the project folder, git remote add origin <URL>.
  3. 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:

  1. On GitHub, click your profile picture → Settings.
  2. In the left sidebar, scroll down to Developer settings.
  3. Personal access tokensTokens (classic)Generate new token (classic).
  4. Note: “Class machine” or similar.
  5. Expiration: No expiration is easiest for a class machine, but 90 days is more secure.
  6. Scopes: Check repo (the top-level box — selects all repo permissions).
  7. Click Generate token.
  8. 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 clone your 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.

  1. Create a GitHub account at github.com if you don’t have one.

  2. Make a Personal Access Token (steps in this handout, under “Personal Access Token setup”).

  3. In your project folder:

    git remote add origin https://github.com/USERNAME/REPO.git
    git push -u origin main
    
  4. Add a README with what your project does and how to run it.

  5. 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) and mixer.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).

  1. Open one of your games (Pong, fruit catcher, etc.).

  2. Load the sound: bounce_sound = pygame.mixer.Sound("bounce.wav").

  3. Play it where appropriate (collision, score, etc.): bounce_sound.play().

  4. (Stretch) Add background music:

    pygame.mixer.music.load("background.ogg")
    pygame.mixer.music.play(-1)
    
  5. (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 a Surface).
  • 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() — calls update() 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.image and self.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’s update().
  • 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 player against every sprite in coins.
  • Returns a list of the coin sprites that the player is touching.
  • The True means “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 Basket class holds both its image AND its movement logic.
  • The Fruit class 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 more coins.remove(coin_rect).
  • global lives in Fruit.update() — the sprite modifies the outer lives variable. 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() and group.draw(screen) — one call updates/draws all sprites.
  • pygame.sprite.spritecollide(sprite, group, kill) — collision against a group.
  • self.kill() — sprite removes itself.
  • Inheritanceclass 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:

  1. Build the basic Player + Coins example from Part A. Walk over the coins. Watch them disappear.

  2. Refactor your fruit catcher with sprite classes (Part B). Same gameplay, cleaner code.

  3. (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 background group drawn first, then mid, then top. Order matters.
  • A Game class 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.
  • Loopsfor i in range(N):.
  • Conditionalsif, else.

What you’ll need today

  • The grid_world.py file 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:

  1. Setup — imports, screen, grid size, colors.
  2. Puzzle definitions — a list of puzzles, each defining start position, goal, and walls.
  3. Movement API — the move_right(), move_left(), etc. functions. They don’t move the character immediately — they add an instruction to a moves list.
  4. 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:

  1. You’ll extend it next week. Adding a new puzzle, a new movement function, or a new feature requires knowing the structure.
  2. 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 for loop 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() after move_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.

  1. Open grid_world.py in Thonny. Run it.

  2. Try each puzzle. Press 1-5 to switch.

  3. Edit solve() to write code that gets the character to the goal. Press SPACE to run.

  4. 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() or move_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:

  1. Add gems to puzzle dicts (a list of cells where gems are):

    {
        "name": "Gem run",
        "start": (0, 0),
        "goal": (7, 7),
        "walls": [],
        "gems": [(3, 3), (5, 5)]
    },
    
  2. Track collected gems in game state:

    collected_gems = set()
    
  3. 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))
    
  4. 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.")
    
  5. 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:

  1. Track facing direction in game state:

    facing = "right"    # or "left", "up", "down"
    
  2. Add the new functions:

    def turn_left():
        moves.append("turn_left")
    
    def turn_right():
        moves.append("turn_right")
    
    def forward():
        moves.append("forward")
    
  3. 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
    
  4. 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:

  1. Add ice to puzzles (a list of icy cells).

  2. 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
    
  3. 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:

  1. Add second character position and goal to puzzles.

  2. 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"))
    
  3. 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:

  1. Use a thing.
  2. Read its code.
  3. Modify it.
  4. Add to it.
  5. 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).

  1. Read through the file. Find the puzzles list.

  2. Add at least one new puzzle. Solve it. Have a friend try.

  3. (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.
  • Conditionalsif, 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/elif dispatch 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.

  1. Build the basic state_demo.py from Part A. Get the title → playing → game_over flow working.

  2. Pick one of your games and add a title screen and game-over screen.

  3. (Stretch) Add a pause state.

  4. (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:

  1. What’s the game? (One sentence. “Snake but with power-ups.” “A two-player tank battle.” “A puzzle where you arrange tiles.”)

  2. What does the player do? (Move with arrows? Click to shoot? Type a word?)

  3. What’s the goal? What’s the lose condition? (Reach a score? Survive 60 seconds? Clear all levels?)

  4. What sprites are in it? (Player? Enemies? Items? Backgrounds? Sketch the screen.)

  5. What states does it have? (Title, playing, game-over at minimum. Maybe more.)

  6. What’s the simplest version? (Build this FIRST. No art polish, no sound, no fancy features. Just core gameplay.)

  7. 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:

  1. Make the change.
  2. Run it. Make sure it works.
  3. Commit.
  4. 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()).
  • spritecollide for 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.

  1. Spend 10-15 minutes answering the seven planning questions.

  2. Create a folder for your project. git init in it. Create main.py. Commit.

  3. 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:

  1. It runs without crashing under normal use.
  2. It does what your plan said it would do — at least the simplest version.
  3. 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( "...") and set_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 status should 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:

  1. Show your title screen. That first frame is the impression. Walk us through the controls if needed.
  2. Play the game. Let us see it actually working.
  3. Hand the keyboard to a classmate if there’s time. Real users, real reactions.
  4. Show your GitHub repo. Open the URL in a browser. Show your git log --oneline.
  5. Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected. Maybe one thing that surprised you.
  6. 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 .html file 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 .exe or .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

SessionIdeaWhat’s new
1Welcome to the web — first HTML pageHTML basics, the browser as runtime
2HTML tags and structureSemantic tags, nesting, attributes
3CSS basics — selectors, colors, fontsThe cascade, selectors, declarations
4The box modelpadding, margin, border
5Flexbox layoutModern layout
6Build a personal homepageHTML + CSS integration project
7Forms — inputs, labels, buttonsForms that look right
8JavaScript — syntax (compared to Python)let/const, semicolons, braces
9The DOM — querySelector and eventsReach into the page from JS
10Build an interactive todo listJS integration project
11localStorage — persistenceState that survives reloads
12Canvas — drawing in JSLike Pygame’s draw, in the browser
13Canvas mini-gameFrame loop, sprites, collision — in the browser
14Fetch + JSON — hitting an APITalk to the world
15GitHub Pages — host your workYour URL on the internet
16Milestone day 1Plan + build
17Milestone day 2 + demoFinish + 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, no node_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:

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:

  1. In your file manager, double-click index.html.
  2. In your browser, File → Open File → pick the index.html.
  3. 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:

  1. Edit the file.
  2. Save.
  3. Reload the browser.
  4. 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 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>.
  • Attributessrc=, 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.

  1. Create a folder for web stuff. Inside it, create a file named index.html.

  2. Type the basic HTML skeleton (DOCTYPE, html, head, title, body).

  3. Add an <h1> and a few paragraphs in the body.

  4. Save. Open index.html in a browser by double-clicking.

  5. 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.html and link to it from index.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&copy; (©), &hearts; (♥), &amp; (&), &lt; (<), &gt; (>). 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>.
  • Attributessrc=, 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>&copy; 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

  1. Open your index.html from last week.

  2. Wrap the title and intro in a <header>.

  3. Add a <nav> with at least one link (it can link back to itself for now — we’ll add real pages later).

  4. Wrap the main content (the sections, lists, etc.) in a <main>.

  5. Wrap each major section in <section>.

  6. 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:

  • &lt;< (less-than)
  • &gt;> (greater-than)
  • &amp;& (ampersand)
  • &copy; — © (copyright)
  • &hearts; — ♥
  • &middot; — · (middle dot)
  • &nbsp; — 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 &lt;h1&gt;.</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 linkshref="#section-id".
  • (Stretch) Multi-page sites with shared navigation.
  • (Stretch) HTML entities&copy;, &amp;, 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.

  1. Open your index.html from Session 1 (or create a new one).

  2. Restructure with semantic tags — <header>, <nav>, <main>, <section>s, <footer>.

  3. Add a navigation menu with at least 3 links.

  4. (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:
    <details>
        <summary>Click to reveal</summary>
        <p>Hidden content!</p>
    </details>
    
    Try it. The browser handles the show/hide automatically.
  • 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, horizontal auto (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, classic
  • Helvetica — sans-serif, clean (Mac)
  • Georgia — serif, friendly
  • Times New Roman — serif, classical
  • Courier New — monospace
  • Comic Sans MS — yes, this exists
  • Verdana — 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:

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 .css files 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.

  1. Create a styles.css file next to your index.html.

  2. Link it in <head>: <link rel="stylesheet" href="styles.css">.

  3. Style at least: body, h1, h2, p, a. Try colors, fonts, and at least one :hover.

  4. 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-radius for rounded corners.
  • box-shadow for 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.

  1. Open your index.html and styles.css from Session 3.

  2. Add *, *::before, *::after { box-sizing: border-box; } at the top of styles.css.

  3. Pick one element (a paragraph, the header). Try adding padding (inside space), margin (outside space), border (the edge line). Reload after each.

  4. 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; }.
  • .btn reusable class for buttons across your site.
  • border-radius: 50% — circles on any square element.
  • box-shadow variations — 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.

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:

  1. The header itself: row with logo on one end and nav on the other (justify-content: space-between).
  2. The nav: row of links with gap between 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; }

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:

  1. Add a navbar at the top with logo + links.
  2. Put your project cards in a row (use flex-wrap).
  3. 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-content or align-items?

Today you learned:

  • Flexbox = modern layout for rows and columns.
  • display: flex; on the parent.
  • flex-direction: row | column; sets the main axis.
  • gap for spacing between items.
  • justify-content for main-axis alignment.
  • align-items for cross-axis alignment.
  • flex: 1 to make items grow equally.
  • flex-wrap for items overflowing to new rows.
  • 100vh for 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.

  1. Add the basic flex example to your page (a .row with three boxes inside).

  2. Try justify-content: center and align-items: center.

  3. Convert your project cards (Session 4) to a flex row using display: flex and flex: 1.

  4. (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-self on individual items to override align-items for that one item.
  • order on items to rearrange visually without changing HTML order.
  • justify-content: space-between patterns — 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).
  • Flexboxdisplay: flex, justify-content, align-items, gap, flex: 1 (Session 5).
  • box-sizing: border-box at 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>&copy; 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: 0 on 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.

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.

  1. Create a folder for your homepage. Create index.html and styles.css.

  2. Type the HTML structure (navbar, hero, sections, footer) from Part A.

  3. Type the CSS in steps 2-7. Reload after each step.

  4. 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 @media queries.
  • 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.
  • Flexboxdisplay: 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 have min, 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 required and 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 (use for=/ 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.
  • :focus pseudo-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.

  1. Build the basic contact form from Part A.

  2. Style it (Part B steps). Make sure focus states work.

  3. (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 + legend for grouping.
  • Custom validation messagespattern=, 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) or const (won’t change).
  • Curly braces { } instead of indentation.
  • Parentheses around if / for / while conditions.
  • Semicolons at the end of statements (technically optional, but include them).
  • No def — use function.

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:

  • Initlet i = 0 (runs once, at start).
  • Conditioni < 10 (checked each iteration).
  • Afteri++ (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) and const (constant). Default to const.
  • Strings with single, double, or backticks. Backticks support ${...} interpolation.
  • === not == for comparison.
  • Conditionals with parens around the condition, braces around the body.
  • Loopsfor (let i = 0; i < N; i++) and for (const x of arr).
  • Functionsfunction name() { ... } or arrow (a, b) => a + b.
  • Arrayspush, pop, length, etc.
  • Objects{key: value} with dot or bracket access.
  • External .js files 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.

  1. Try the syntax examples in Part A. Run each in the console. Notice the output.

  2. Build the test HTML page from Part B with the <script> block.

  3. Move the JS to an external app.js file.

  4. 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 methodsmap, filter, reduce, forEach. Practice converting Python loops to these.
  • try/catch for error handling.
  • JSON.parse and JSON.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.log for 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:

  1. <h1 id="title">Hello!</h1> — the actual HTML element.
  2. 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:

  1. State — a variable (count).
  2. DOM references — find elements (display, buttons).
  3. Event listeners — wire up buttons.
  4. 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.X directly. 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:

  1. createElement("li") — make a new <li> element (not yet in the page).
  2. Set its content.
  3. appendChild adds 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(...) + appendChild for 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.

  1. Build the basic dom-test.html from Part A. Get the title to change when you click the button.

  2. Build the counter from Part A. Plus, minus, reset.

  3. Build the greeting from Part B with live updates and dark mode.

  4. (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:
    colorInput.addEventListener("input", () => {
        document.body.style.backgroundColor = colorInput.value;
    });
    
    (One time when .style.X is 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.querySelector and addEventListener (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 createElement and appendChild.
  • 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.

  1. Create a folder. Inside, index.html, app.js, styles.css.

  2. Build the HTML shell from Step 1.

  3. Build the add-todo logic from Step 2.

  4. Style with the CSS from Step 3.

  5. 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, drop events).
  • 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.stringify and JSON.parse (Session 8 stretch — we’ll cover them properly today).
  • DOM manipulationcreateElement, 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 (returns null), 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.parse for objects and arrays.
  • getItem returns null if 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.

  1. Open app.js. Add the saveTodos() and loadTodos() functions from the handout.

  2. Refactor add logic into an addItem(text, done) function that both form-submit and load can use.

  3. Call saveTodos() after every change. Call loadTodos() once at startup.

  4. Add some todos, reload, see them persist.

  5. (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.
  • x increases to the right.
  • y increases 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 either fill() or stroke() 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.

  1. Build the canvas-test.html with the basic rectangle and circle.

  2. Build the scene from Part A — sky, ground, sun, house.

  3. Build your own scene — pick a subject from the list, use at least 6 shapes.

  4. (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 effectsctx.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).
  • localStorage for 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 index i (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/keyup listeners + a keys object.
  • 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.

  1. Build the bouncing rectangle from Part A. See it bounce.

  2. Add keyboard control. Move the square with arrows.

  3. Build the catcher game from Part B.

  4. (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.
  • Soundnew 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 use await inside.
  • 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 we await it too.
  • Then data.fact is 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 APIhttps://www.boredapi.com/api/activity → returns a random activity to do.
  • Pokemonhttps://pokeapi.co/api/v2/pokemon/charizard → returns Pokemon data. Try different names.
  • NASA APODhttps://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 APIhttps://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.

  1. Build the cat-fact page from Part A. Click the button, see facts.

  2. Build the dog-photo page. Click for new dogs.

  3. Build the combo (dog + joke).

  4. (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 terminalcd, 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:

  1. Make a folder for it on your machine.
  2. Inside, you should have at least index.html and styles.css.
  3. From the terminal:
    $ cd ~/projects/homepage    # or wherever yours is
    $ git init
    $ git add .
    $ git commit -m "Initial homepage"
    
  4. On GitHub, create a new repo (let’s say named homepage).
  5. 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

  1. Go to your repo on GitHub.
  2. Click Settings (top of the repo).
  3. In the left sidebar, click Pages.
  4. Under Source, pick Deploy from a branch.
  5. Under Branch, pick main and / (root).
  6. 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.

  1. Edit your index.html. Change a heading or add a line.
  2. Save. Reload your local file to verify.
  3. From terminal:
    $ git add .
    $ git commit -m "Update homepage"
    $ git push
    
  4. Wait 30-60 seconds.
  5. 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 named USERNAME.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

  1. Make sure the project is in its own folder with at least index.html.
  2. git init, commit, push to a new GitHub repo.
  3. Enable Pages on that repo.
  4. Visit the URL.

15 minutes per project once you have the rhythm.

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 &amp; 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.

  1. Push your project to a new GitHub repo.

  2. In repo Settings → Pages, set source to “main” branch, “/ (root)” folder.

  3. Wait 1-2 minutes.

  4. Visit https://USERNAME.github.io/REPONAME/.

  5. (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.io as the URL — needs a special repo name.
  • Custom domain if you have one.
  • Set up an about.html and projects.html as 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:

  1. What’s the project? (One sentence. “A recipe collector.” “A canvas paint app.” “A weather widget.”)

  2. What does the user do with it? (How do they interact?)

  3. What HTML elements does it need? (Forms? Lists? Canvas? Image grid?)

  4. What does it look like? (Sketch the layout on paper. Where’s the navbar, content, footer?)

  5. What’s the JavaScript doing? (Click handlers? DOM updates? Fetch from an API?)

  6. Does it need persistence? (localStorage? What state?)

  7. What’s the simplest version? (Build this FIRST. Just the core. No fancy features.)

  8. 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):

  1. Create a new public repo on GitHub.
  2. Connect:
    $ git remote add origin https://github.com/USERNAME/my_project.git
    $ git push -u origin main
    
  3. 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:

  1. Make the change.
  2. Open index.html in browser. Test.
  3. git add + git commit -m "...".
  4. (Optional, but encouraged) git push — deploy the in-progress version.
  5. 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.

  1. Spend 10-15 minutes answering the eight planning questions.

  2. Set up the project folder + index.html + git init + commit.

  3. Push to a new GitHub repo. Enable Pages.

  4. Build the simplest version. Commit and push as you go.

About 60-90 minutes total. By next week you should have:

  • An index.html with 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 @media queries.
  • 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:

  1. It runs without crashing under normal use.
  2. It does what your plan said — at least the simplest version.
  3. 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.png next to index.html and 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 pushedgit status should 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:

  1. Show your URL. Open it on the projector. Walk us through the page.
  2. Use it live. Click buttons. Type things. Let us see it actually working.
  3. Have a classmate visit your URL on their machine. Real test of “shareable to anyone.”
  4. Tell us about it. What is it? Who is it for?
  5. Show your GitHub repo. Open the URL in a browser. Show your git log.
  6. Tell us one thing that was hard. Bug stories, design decisions, things that took longer than expected.
  7. 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

SessionIdeaWhat’s new
1Welcome to Flask — first serverThe request/response cycle, install, hello world
2Routes and URL parametersMultiple routes, dynamic URLs
3Templates — Jinja2Variables, loops, conditionals in HTML
4Static files + base templatesCSS, images, template inheritance
5Forms and POST requestsrequest.form, GET-then-POST
6Sessions and flash messagessession, cookies, user feedback
7SQLite — your first databaseWhat a database is, the sqlite3 module
8Database in Flask — CRUDSELECT/INSERT/UPDATE/DELETE in routes
9User accountsSignup, login, logout, password hashing
10Notes app — foundationTwo-table schema, per-user filter, list + create
11Notes app — polishView, edit, delete, multi-user + security demo
12DeploymentReal public hosting (PythonAnywhere)
13Milestone day 1Plan + build your full-stack app
14Milestone day 2 + demoFinish + 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 terminalcd, 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:

  1. Your browser sends a request to Google’s servers.
  2. Google’s server looks at the URL, processes the request.
  3. The server sends back HTML (and CSS, JS, images, etc.).
  4. 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 flask to 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.

  1. pip install flask.

  2. Create app.py with the basic Flask hello-world.

  3. Run with python app.py.

  4. Visit http://127.0.0.1:5000 in a browser.

  5. 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 portapp.run(debug=True, port=8000).
  • Multiple files — Flask apps can be split across modules. Out of scope for today.
  • Try flask from terminal — instead of python app.py, set FLASK_APP=app and flask 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 basicsFlask(__name__), @app.route("/"), app.run(debug=True).
  • The request-response cycle — browser asks, server answers.
  • Returning HTML strings.
  • F-stringsf"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:

  1. Match URLs that look like /hello/SOMETHING.
  2. Capture the SOMETHING part as a variable called name.
  3. 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:

  • POKEMON dictionary — 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() returns None if 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.args for 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).

  1. Build the basic app.py with /, /hello/<name>, and /double/<int:n> routes.

  2. Test in browser with several different URLs.

  3. (Stretch) Build the Pokemon viewer.

  4. (Stretch) Add a /search route using request.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 basicsFlask(__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 reads templates/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.

  1. Create a templates/ folder next to your app.py.

  2. Build templates/home.html with at least one {{ variable }}.

  3. Update app.py to use render_template.

  4. Add a {% for %} loop and {% if %} conditional.

  5. (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_datetime filter (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

  • Templatestemplates/ 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.csshttp://127.0.0.1:5000/static/style.css
  • static/images/logo.pnghttp://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>&copy; 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

  1. {% extends "base.html" %} — child says “I’m based on this template.”
  2. {% block content %}...{% endblock %} — child replaces the block with new content.
  3. 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.html and seeing every page update feel powerful?
  • For the kids who used url_for for 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.

  1. Create a static/ folder. Put a CSS file in it.

  2. Link from your templates with url_for.

  3. Create templates/base.html with {% block %} tags.

  4. 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_for for 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 @media queries.
  • Error templatestemplates/404.html, registered with @app.errorhandler(404).
  • Favicon — drop static/favicon.ico and 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:

  1. GET / → show the form + existing messages.
  2. POST / → process, store, redirect back to GET /.
  3. 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.method to 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.getlist for 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.

  1. Build a simple form template with name and message inputs.

  2. Set up app.py with a single route handling both GET and POST.

  3. Use request.form.get(...) to read the data.

  4. Implement the POST-Redirect-GET pattern with redirect(url_for(...)).

  5. (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 POSTrequest.form, the POST-Redirect-GET pattern.
  • Templatesrender_template, {{ }}, {% if %}.
  • Python dictionariessession works 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.”

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_key required 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.

  1. Add app.secret_key = "..." to app.py.

  2. Use session["name"] = ... and session.get("name") to remember a visitor’s name.

  3. Add a logout route that clears the session.

  4. (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 for with 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 users table, a posts table.
  • 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:

idnameage
1Alex12
2Sam10
3Pat14

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 sqlite3 module).
  • 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 called test.db exists 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 named pets.
  • 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 — no NOT 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.py running. 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 TABLE to define structure.
  • INSERT to add rows.
  • SELECT to read; WHERE to filter.
  • UPDATE to change; DELETE to 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.

  1. Run a few sqlite3 commands in the Python shell to create a table and insert rows.

  2. Build the pets.py script from Part B.

  3. Run it twice — verify data persists.

  4. (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 SQLORDER BY, LIMIT, LIKE, IN, BETWEEN.
  • Aggregate functionsCOUNT, SUM, AVG, MAX, MIN.
  • Multiple tables with foreign keys + JOINs.
  • TransactionsBEGIN, 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 basicsconnect, 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. So row["name"] works, not just row[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 5Session 8
messages = [] (in-memory)messages table in SQLite
messages.append(...)INSERT INTO messages
for m in messages:SELECT * FROM messages
Lost on restartPersists
One server onlyShared 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.Row for dict-like rows.
  • init_db() on startup to ensure schema.
  • INSERT in routes for creating data.
  • SELECT in routes for reading.
  • UPDATE for editing.
  • DELETE for 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.

  1. Take the Session 5 guestbook.

  2. Add get_db() and init_db().

  3. Replace messages = [] with SQL operations.

  4. Run, post messages, restart, verify they persist.

  5. (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

  • Sessionssession["key"] = value (Session 6).
  • SQLite + Flaskget_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:

  • UNIQUE on the username column — the database refuses duplicates. We catch sqlite3.IntegrityError to tell the user.
  • current_user() helper — reads session["user_id"], looks up the user, returns the row (or None if not logged in).
  • @app.context_processor — makes current_user automatically 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

  1. Visit /. “Welcome! Please log in or sign up.”
  2. Click sign up. Pick a username and password.
  3. Submit. “Account created! Please log in.”
  4. Click log in. Use the same credentials.
  5. Submit. “Welcome back, alex!” — and the nav shows “Logged in as alex” with a Log out button.
  6. Visit /. “Welcome, alex!”
  7. Log out. Back to “stranger” state.
  8. Try to log in with the wrong password. “Invalid username or password.”
  9. 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_hash in 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.security has the right functions: generate_password_hash and check_password_hash.
  • The users table with username, password_hash, created_at.
  • UNIQUE constraint 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_required decorator 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.

  1. Build the full app.py from Part B.

  2. Build all four templates.

  3. Test the signup → login → logout flow.

  4. Inspect auth.db to see hashed passwords.

  5. (Stretch) Add @login_required.

About 60-90 minutes — this is a substantial session.

Stretch and extension ideas

  • @login_required decorator 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_required decorator (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

  1. Sign up. Log in.
  2. Click “Your notes.” Empty list.
  3. Click “+ New note.” Type a title and content. Save. Note appears in the list.
  4. 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.

  1. Build the foundation from Part A — both tables, auth (copy from Session 9).

  2. Build the list and create routes from Part B.

  3. Build the templates.

  4. 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

  1. Log in. Open the notes list.
  2. Click a note. View page loads.
  3. Click Edit. Form pre-fills. Change the title. Save. Updated time changes.
  4. 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:

  1. You sign up as User A. Your buddy as User B. (Same app, two accounts — different browsers, or one browser plus an incognito window.)
  2. Each create some notes.
  3. 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:

  1. Log in as User B.
  2. Create a note. Click into it. Look at the URL — something like http://127.0.0.1:5000/notes/3. Note the number.
  3. Now visit a smaller number — say, /notes/1. (User A’s notes have ID 1 and 2; you’re guessing.)
  4. 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.

  1. Add the view, edit, and delete routes from Part A.

  2. Update notes_list.html to link to the view page.

  3. Test the full flow: list, create, view, edit, delete.

  4. Try the URL-tampering test with two accounts.

About 60-90 minutes.

Stretch and extension ideas

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

  1. Go to pythonanywhere.com.
  2. Click Pricing & signup.
  3. Pick the “Beginner” account (free).
  4. Fill in username + email + password.
  5. 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:

  1. Push it to GitHub (Phase 6 Session 7 refresher).

In PythonAnywhere:

  1. Click the Consoles tab.
  2. Start a Bash console.
  3. In the console:
    $ git clone https://github.com/YOUR-USERNAME/notes_app.git
    
  4. cd notes_app and ls to verify your files are there.

Step 3 — Set up the web app

  1. Click the Web tab.
  2. Click Add a new web app.
  3. Click Next through the domain setup (free accounts get <username>.pythonanywhere.com).
  4. Pick “Manual configuration” (not “Flask” — the auto-Flask option is for old setups).
  5. Pick Python 3.10 (or the latest available).
  6. 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. Replace YOUR-USERNAME with 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 called application.

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:

  1. Commit and push to GitHub.
  2. On PythonAnywhere, in the bash console:
    $ cd notes_app
    $ git pull
    
  3. In the Web tab, click Reload.
  4. 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 skipped pip install --user flask (Step 5 above). Run it. Reload.
  • ModuleNotFoundError: No module named 'app' → Your WSGI file’s project_home path 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 pull on 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 .gitignore and 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.
  • --user install for dependencies.
  • git pull + Reload for updates.
  • Absolute paths (__file__-based) for database files.
  • .gitignore to keep db files out of Git.
  • Environment variables for secrets.
  • requirements.txt for 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:

  1. Push your notes app to GitHub if you haven’t.

  2. In PythonAnywhere bash console: git clone your repo.

  3. Add a new web app (Manual configuration, Python 3.10).

  4. Edit the WSGI file to point at your folder and import your app.

  5. pip install --user flask.

  6. Reload the web app.

  7. Visit the URL.

About 45-60 minutes. By the end you should have a public URL.

Stretch and extension ideas

  • Real secret_key from env var.
  • Absolute path for DB.
  • requirements.txt with all deps.
  • .gitignore for *.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:

  1. What’s the app? (One sentence. “A recipe-sharing site.” “A polling app.” “A reading log.”)

  2. Who’s it for? (Yourself? Class? General public?)

  3. Does it need user accounts? (Almost always yes for Phase 8 milestones.)

  4. What does each user do? (Sign up, log in, then… what?)

  5. What database tables? (users plus what? Each table: name + columns.)

  6. What routes? (Each URL + what it does. At minimum: home, signup, login, logout, list, create, view, edit, delete.)

  7. What templates? (One per route, usually.)

  8. What’s the simplest version? (Build this FIRST. No fancy features. Just core flow.)

  9. 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:

  1. Create a new public repo on GitHub.
  2. git remote add origin ... + git push -u origin main.

Then deploy to PythonAnywhere right now, before any real code:

  1. PythonAnywhere bash console: git clone your repo.
  2. Add a new web app (Manual config, Python 3.10).
  3. Edit WSGI file.
  4. pip install --user flask.
  5. Reload.
  6. 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:

  1. Make the change locally.
  2. Test in browser at 127.0.0.1:5000.
  3. Commit + push.
  4. PythonAnywhere bash: cd my_app && git pull.
  5. PythonAnywhere web tab: Reload.
  6. 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.

  1. Spend 10-15 minutes answering the nine planning questions.

  2. Set up your project + GitHub + PythonAnywhere.

  3. Build the simplest version (signup + login at minimum).

  4. 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:

  1. It runs without crashing under normal use.
  2. It does what your plan said — at least the simplest version.
  3. 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_key from env var if you want production polish.
  • .gitignore for *.db so 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 status should 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:

  1. Show your URL. Open it on the projector.
  2. Sign up live as Mr. Eric (or someone else fresh).
  3. Use your app. Walk us through what it does.
  4. Have a classmate sign up on their machine during your demo.
  5. Show your repo. GitHub URL, git log.
  6. Tell us about it. What it is, who it’s for, what was hard.
  7. Show the database if you can (DB Browser locally, or a screenshot).
  8. 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:

  1. Scratch — visual programming. Sequences, loops, events, conditionals, variables.
  2. Python Turtle — typing instead of dragging. Functions. Same ideas, real syntax.
  3. Python basics — strings, lists, dicts, files, error handling. The full beginner Python.
  4. Intermediate Python + CLI + Git — multi-file programs, classes, version control, testing.
  5. customtkinter — desktop GUIs. Real apps with windows, buttons, forms.
  6. Pygame — 2D games. Frame loop, sprites, collisions, sound, your custom grid-world.
  7. HTML/CSS/JavaScript — the front-end. The browser as runtime.
  8. 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 fetch to 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 say block (Scratch) or a print() (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.