Python Academy

Learn Python by programming a robot.

A self-paced curriculum built around FLL missions. Ten short lessons that take you from hello, world to driving a SPIKE Prime robot through a real challenge. Open to any student โ€” no class, no signup, just open a lesson and start.

10
Lessons, ~20 min each
2
Modules on SPIKE Prime
4-8
Grades it's built for
How this works

Open a lesson. Read it. Try it on your robot.

Each lesson is a small bite โ€” a goal, a few concepts, real Python code you can copy, and one mini challenge. You don't need an instructor. You don't need to do them in order, but it helps. You do need a SPIKE Prime kit โ€” the robot we use in FLL Challenge. All the code in this course runs on the SPIKE Prime hub through the LEGO Education SPIKE app. Setup steps are right below.

Stuck on the code? Slow down and re-read the lesson โ€” almost every failure here is a typo or a swapped port letter. Each lesson shows exactly what your output should look like, so you can spot the difference fast.

Before you start

Set up your robot.

Every lesson in this course โ€” Module 1 and Module 2 โ€” runs on the SPIKE Prime hub through the LEGO Education SPIKE app. You don't need a separate Python editor on your computer. Get the hub talking to the app, and you're ready for Lesson 1.

What you need

  • A SPIKE Prime kit (the yellow hub + motors, sensors, cables, charged battery)
  • A computer or tablet โ€” Mac, Windows, iPad, Chromebook, or Android
  • The Micro-USB cable that came with the kit (Bluetooth works too once paired)
  • The LEGO Education SPIKE app installed (step 1 below)
  1. Install the LEGO Education SPIKE app.

    Download it from education.lego.com/spike-app. It's free. Pick the version for your computer or tablet.

    Open the app once it's installed. You can click around โ€” you don't need to log in.

  2. Power on the SPIKE Prime hub.

    Hold the round button in the middle of the hub for about a second. You'll see the LEGO logo, then a 5ร—5 grid of yellow lights โ€” that's the home screen.

    If the hub doesn't turn on, plug in the Micro-USB cable to charge it for a few minutes and try again.

  3. Connect the hub to the app.

    Plug the hub into your computer with the Micro-USB cable. In the SPIKE app, click the connect icon (top of the screen) โ€” pick your hub from the list.

    Bluetooth also works: turn Bluetooth on, hit connect in the app, pair when it finds the hub.

  4. Create a new Python project.

    In the app: New Project โ†’ Python. Important โ€” pick Python, not Word Blocks. The middle area of the screen is where you'll paste lesson code.

  5. Try Lesson 1 as your first test.

    Paste this into the editor and hit the play (โ–ถ) button at the bottom:

    print("Hello, robot!")

    If you see "Hello, robot!" in the console panel at the bottom of the app, you're set. Move on to Lesson 1 below.

When something's not working

  • Hub won't connect: close the app fully, unplug the USB cable, count to 5, plug it back in and reopen the app.
  • You pasted code but the play button is grey: you're probably in a Word Blocks project. Start a new project and pick Python.
  • Nothing happens when you hit play: check the hub is awake โ€” press the round button. If the lights are off, hold it for a second.
  • Battery is low: a small lightning bolt appears on the hub's screen. Plug it in โ€” you can keep coding while it charges.
  • Still stuck? Run the diagnostic program below โ€” it'll tell you exactly which piece isn't responding.

A tiny check program. It tries each piece of the robot in turn and reports what it found. Watch both the hub screen (shows test numbers) and the console panel in the SPIKE app (shows sensor readings).

โ–ถ SPIKE Prime ยท Python ยท Diagnostic
from hub import port, light_matrix
import runloop
import motor_pair
import color_sensor
import distance_sensor

async def main():
    print("=== LR robot diagnostic ===")

    # Test 1 โ€” hub is alive and running Python
    await light_matrix.write("1")
    print("1: Hub alive. (You should see '1' on the hub screen.)")
    await runloop.sleep_ms(800)

    # Test 2 โ€” color sensor on port B
    await light_matrix.write("2")
    seen = color_sensor.color(port.B)
    print("2: Color sensor on B reads:", seen)
    await runloop.sleep_ms(800)

    # Test 3 โ€” distance sensor on port D
    await light_matrix.write("3")
    far = distance_sensor.distance(port.D)
    print("3: Distance sensor on D reads:", far, "mm")
    await runloop.sleep_ms(800)

    # Test 4 โ€” drive motors on A and E nudge forward 90 degrees
    await light_matrix.write("4")
    motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, 90, 0, velocity=200)
    print("4: Motors on A + E nudged forward.")

    # Done โ€” smiley face on the hub if everything ran
    light_matrix.show_image(light_matrix.IMAGE_HAPPY)
    print("=== Diagnostic done. Smiley = all four tests ran. ===")

runloop.run(main())

What each result tells you

  • You see nothing โ€” no numbers, no console output: the program never started. Hub isn't connected to the app, or you're in a Word Blocks project, not Python.
  • Hub shows "1" but the console is empty: the program runs but the SPIKE app's console panel isn't visible. Find the console toggle in the app (usually a small button near the play button).
  • Test 2 shows a weird number (or hangs): the color sensor isn't on port B, or it's facing the wrong way. Move it to B and point the lens down at something colored.
  • Test 3 shows -1 or a huge number: the distance sensor isn't on port D, or there's nothing in front of it. Put your hand 20 cm in front and re-run.
  • Test 4 โ€” wheels don't spin: the drive motors aren't on ports A and E. Swap the cables and try again.
  • The smiley face appears at the end: everything works. The earlier lesson that failed probably has a typo โ€” re-paste it and try again.

If the diagnostic runs but a specific lesson still fails, compare your code line-by-line against the lesson. The fix is almost always a typo โ€” a missing await, a swapped port letter, or indentation that's off. LEGO's working sample programs (linked at the bottom of the page) show the patterns in action if you need a second reference.

Look at your robot. For each port (A through F), what do you think is plugged in? Type your guess into the MY_GUESS dictionary at the top of the code, then hit run. The script reads each port and tells you whether reality matches what you said.

Valid guesses: "motor", "color", "distance", or "nothing".

โ–ถ SPIKE Prime ยท Python ยท Port scanner
from hub import port, light_matrix
import runloop
import motor
import color_sensor
import distance_sensor

# === EDIT THIS to match what YOU think is plugged in ===
# Choices: "motor", "color", "distance", "nothing"
MY_GUESS = {
    "A": "motor",
    "B": "color",
    "C": "motor",
    "D": "distance",
    "E": "motor",
    "F": "nothing",
}

def looks_like(c_val, d_val, m_val):
    # Heuristic โ€” what kind of device is this port reporting?
    if 40 < d_val < 2000:
        return "distance"
    if c_val is not None and 0 <= c_val <= 10:
        # Color sensor returns a small int โ€” but so does "no device" sometimes.
        # Wave a colored block in front to confirm. Best guess: color.
        return "color"
    if m_val != 0:
        return "motor"
    return "nothing"

async def main():
    print("=== Port check โ€” your guess vs reality ===")
    print("Spin each wheel by hand before running, so motors register motion.")
    print()

    ports = [(port.A, "A"), (port.B, "B"), (port.C, "C"),
             (port.D, "D"), (port.E, "E"), (port.F, "F")]

    matches = 0
    for p, name in ports:
        guess = MY_GUESS[name]
        c = color_sensor.color(p)
        d = distance_sensor.distance(p)
        m = motor.relative_position(p)
        actual = looks_like(c, d, m)

        if actual == guess:
            print("Port", name, ": you said", guess, "-- looks right โœ“")
            matches += 1
        else:
            print("Port", name, ": you said", guess, "-- looks more like", actual)
            print("   (color=", c, " dist=", d, "mm  motor_pos=", m, ")")

        await runloop.sleep_ms(200)

    print()
    print("Score:", matches, "/ 6 ports match your guess.")
    if matches == 6:
        light_matrix.show_image(light_matrix.IMAGE_HAPPY)
    else:
        light_matrix.write(str(matches))

runloop.run(main())

If your guess is wrong

  • The script says "looks more like motor" but I see no motor on that port: spin every wheel by hand right before you run the script โ€” motors only register if they've moved recently. Also check the cable is fully clicked in on both ends.
  • "Looks more like color" but I'm sure that port is empty: the color sensor sometimes reads as if it's there even when it's not โ€” it's a known SPIKE Prime quirk. Cover the suspected color sensor with your hand and re-run. If the number changes, it's really a color sensor.
  • Distance reads -1 or 0 on a port that should have the distance sensor: the sensor is probably plugged in upside-down or on a different port. Try moving it.
  • Edit and re-run. Update MY_GUESS with what you learned, then run again. Goal: 6/6.

Once you know what's on each port, you have two paths through the lessons:

Either replug each cable so it matches the port letter in the lesson (the lesson code then works as-is), or find the port.X letters in each lesson and change them to your build's ports. Both are valid โ€” pick whichever feels easier to you.

Module 1

Python Basics

Start here if you've never written code before. These five lessons cover the building blocks of every Python program โ€” variables, decisions, loops, functions. You'll run them on the SPIKE Prime hub itself, and read the output in the SPIKE app's console panel.

1

Hello, Python

Goal: Write your very first program. Time: ~10 min.

  • How Python runs a program โ€” top to bottom, line by line.
  • The print() function โ€” how to make Python say things.
  • That code is just instructions written in a very picky language.
โ–ถ Python
print("Hello, robot!")
print("My name is " + "Lucy")
print(7 + 3)
Inputs
None โ€” the values are hard-coded into the code.
Process
  1. Print the message "Hello, robot!".
  2. Join "My name is " and "Lucy" into one piece of text, then print it.
  3. Add 7 + 3 and print the result.
Expected output
Hello, robot!
My name is Lucy
10

Python runs line by line, top to bottom โ€” it's a calculator that happens to know how to do everything else too.

Try it yourself

Write three print() lines: your name, your favorite mission from FLL, and the answer to 12 * 8. Then run your program.

2

Variables and Numbers

Goal: Store information your program can use later. Time: ~15 min.

  • A variable is a name that holds a value โ€” like a labeled box.
  • Numbers (int, float) and text (str) are different types.
  • Math operators: + - * / % โ€” that last one is "remainder."
โ–ถ Python
# Variables hold values you can use later
robot_name = "Sparky"
wheel_diameter_cm = 5.6
laps = 3

# You can do math with them
distance_per_lap = 3.14 * wheel_diameter_cm
total_distance = distance_per_lap * laps

print(robot_name + " traveled " + str(round(total_distance, 1)) + " cm")
Inputs
  • robot_name = "Sparky"
  • wheel_diameter_cm = 5.6
  • laps = 3
Process
  1. Store the robot's name, wheel size, and lap count in variables.
  2. Calculate distance_per_lap = 3.14 ร— 5.6 (about one wheel's circumference).
  3. Multiply by laps to get total_distance.
  4. Round to 1 decimal, glue everything into a sentence, and print.
Expected output
Sparky traveled 52.8 cm
Try it yourself

Make a variable missions_scored set to 4 and points_per_mission set to 25. Print "Total points: ___" โ€” your program does the math.

3

Making Decisions (if / else)

Goal: Make your program choose what to do. Time: ~20 min.

  • if, elif, and else let your program choose.
  • Comparison operators: == < > <= >= !=.
  • Why indentation matters in Python (it's how Python knows what's "inside" an if).
โ–ถ Python
battery = 42

if battery > 70:
    print("Battery is full โ€” run all missions")
elif battery > 30:
    print("Battery is okay โ€” save the long missions")
else:
    print("Battery is low โ€” charge before the next round")
Inputs
battery = 42
Process
  1. Check if battery > 70 โ†’ no, it's only 42, so skip.
  2. Check if battery > 30 โ†’ yes, 42 is greater than 30, so print the "okay" message.
  3. The else branch never runs.
Expected output
Battery is okay โ€” save the long missions

Indentation isn't just for looks here โ€” Python uses it to know which lines belong to the if. Always 4 spaces.

Try it yourself

Make a variable color_seen. If it's "red", print "Stop!". If it's "green", print "Go!". Otherwise print "Wait for the right color."

4

Loops (doing things many times)

Goal: Stop copy-pasting. Let the computer repeat. Time: ~20 min.

  • for loops โ€” repeat a fixed number of times.
  • while loops โ€” repeat until something is true.
  • range() โ€” Python's way of counting.
โ–ถ Python
# Repeat a fixed number of times
for i in range(5):
    print("Lap " + str(i + 1))

# Or repeat until something happens
points = 0
while points < 100:
    points = points + 25
    print("Scored! Total: " + str(points))
Inputs
None โ€” the lap count (5) and target points (100) are hard-coded.
Process
  1. For loop: count from 0 to 4 (that's what range(5) gives you), print "Lap" plus that number + 1.
  2. Set points = 0.
  3. While loop: as long as points is under 100, add 25 to it and print the running total.
  4. When points hits 100, the while condition becomes false and the loop stops.
Expected output
Lap 1
Lap 2
Lap 3
Lap 4
Lap 5
Scored! Total: 25
Scored! Total: 50
Scored! Total: 75
Scored! Total: 100
Try it yourself

Print the numbers 1 through 10, but for every number divisible by 3, print "fizz" instead. Hint: use % to get the remainder.

5

Functions (your own building blocks)

Goal: Wrap your code into reusable chunks. Time: ~20 min.

  • How to define your own functions.
  • Parameters (information you pass in) and return values (information you get back).
  • Why functions are the most powerful idea in this whole course.
โ–ถ Python
def mission_score(missions_completed, bonus):
    base = missions_completed * 25
    return base + bonus

# Now call your function any time you need a score
round_1 = mission_score(4, 10)
round_2 = mission_score(6, 0)

print("Round 1: " + str(round_1))
print("Round 2: " + str(round_2))
Inputs
mission_score(missions_completed, bonus) โ€” takes two numbers per call.
Call 1: (4, 10). Call 2: (6, 0).
Process
  1. Define mission_score: multiplies missions_completed by 25 for the base, adds bonus, returns the total.
  2. Call mission_score(4, 10) โ†’ 4 ร— 25 + 10 = 110. Save to round_1.
  3. Call mission_score(6, 0) โ†’ 6 ร— 25 + 0 = 150. Save to round_2.
  4. Print both rounds.
Expected output
Round 1: 110
Round 2: 150

Every robot mission in Module 2 will start by you writing a function. Get comfortable here.

Try it yourself

Write a function can_attempt(battery, time_left) that returns True if battery is over 30 and there are at least 20 seconds left, otherwise False.

Module 2

Programming a Robot

Now your Python runs on a robot. These five lessons use SPIKE Prime โ€” the LEGO platform we use in FLL Challenge โ€” and they map straight to the kind of work an FLL team does in the season. Each lesson builds on the previous one. You'll write more code, but the ideas are the same as Module 1.

Build the robot first.

Every Module 2 lesson assumes you've built LEGO's standard Driving Base โ€” the basic robot that comes with the SPIKE Prime build instructions. If you haven't built it yet, do that first. It takes about an hour and the official instructions are step-by-step.

Port reference (where things plug in on the Driving Base): left motor โ†’ A, right motor โ†’ E, color sensor โ†’ B, distance sensor โ†’ D, arm motor (when used) โ†’ C.

Driving Base build PDF → LEGO Training Camp 1 →

Reading the numbers in Module 2 code

Degrees of rotation: motors are measured in degrees, not centimeters. One full wheel turn = 360ยฐ. For the standard Driving Base, the wheels are ~17.5 cm around, so:

10 cm โ‰ˆ 206ยฐ  ยท  20 cm โ‰ˆ 411ยฐ  ยท  30 cm โ‰ˆ 617ยฐ  ยท  formula: (cm รท 17.5) ร— 360

Steering: in move_for_degrees, the steering value controls direction โ€” 0 is straight, 100 is hard right, -100 is hard left.

Velocity: degrees per second โ€” 100 slow, 300-500 medium, 1000 fast.

Async & await: every robot program is wrapped in async def main(): and any line that takes time gets an await in front. The last line is always runloop.run(main()). That's just how the SPIKE app runs Python โ€” pattern-match the shape, you don't need to fully understand why.

6

Move and Turn

Goal: Drive the robot forward, back, and around. Time: ~25 min.

  • The MotorPair โ€” driving two motors together as one robot.
  • Driving by distance (centimeters) vs by time (seconds).
  • Why "turn 90 degrees" is harder than it sounds.
โ–ถ SPIKE Prime ยท Python
from hub import port
import runloop
import motor_pair

# Pair your two drive motors
motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def main():
    # Drive forward about 30 cm (30 รท 17.5 ร— 360 โ‰ˆ 617 degrees)
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, 617, 0, velocity=500)

    # Turn right (steering = 100 spins in place to the right)
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, 180, 100, velocity=300)

runloop.run(main())
Inputs
  • Drive motors plugged into port.A (left) and port.E (right) on the Driving Base.
  • Forward: 617 degrees at velocity 500 with steering 0 (straight).
  • Turn: 180 degrees at velocity 300 with steering 100 (sharp right).
Process
  1. Pair motors A and E so they move together as one unit (motor_pair.PAIR_1).
  2. Drive forward 617ยฐ โ†’ about 30 cm of travel at medium speed.
  3. Turn in place 180ยฐ with steering 100 โ†’ robot rotates roughly 90ยฐ to the right.
  4. runloop.run(main()) at the bottom is what actually starts the program.
Expected output
The robot drives straight forward about 30 cm, pauses, then spins in place roughly 90ยฐ to the right. Nothing prints to the console.
Try it yourself

Drive your robot in a square: forward 30 cm, turn 90ยฐ, repeat 4 times. Bet you it doesn't end exactly where it started โ€” that's the gyro problem we fix in Lesson 7.

7

Sensors โ€” Color, Distance, Gyro

Goal: Let your robot see the world. Time: ~30 min.

  • Reading the color sensor โ€” useful for line following and station detection.
  • Reading the distance (ultrasonic) sensor โ€” knowing what's in front of you.
  • The gyro โ€” why it makes turns way more accurate than time-based turns.
โ–ถ SPIKE Prime ยท Python
from hub import port
import runloop
import color_sensor
import color
import distance_sensor

async def main():
    # Read what color the sensor on port B sees right now
    seen = color_sensor.color(port.B)
    print("I see color:", seen)

    # Check if that color is red
    if seen is color.RED:
        print("That is red!")

    # Read distance to the nearest object in millimeters
    far = distance_sensor.distance(port.D)
    print("Distance:", far, "mm")

runloop.run(main())
Inputs
  • Color sensor plugged into port.B.
  • Distance sensor plugged into port.D.
  • Whatever the sensors actually see when you run the program.
Process
  1. Ask the color sensor on port B what color it sees right now.
  2. Print whatever it returned.
  3. Compare it against color.RED using is. If it matches, print a follow-up.
  4. Ask the distance sensor on port D how far the nearest object is, in millimeters. Print it.
Expected output

Depends on what's in front of the sensors. If the color sensor is over a red block and the distance sensor sees a wall 150 mm away:

I see color: 9
That is red!
Distance: 150 mm

Use the color module constants for comparison: color.BLACK, color.WHITE, color.RED, color.GREEN, color.BLUE, color.YELLOW, color.MAGENTA, etc.

Try it yourself

Write a loop that reads the distance sensor 10 times in a row, half a second apart, and prints each reading. Notice how stable (or wobbly) the numbers are.

8

Smart Driving (sensors + decisions)

Goal: Drive until something happens. Time: ~30 min.

  • Combining Module 1 ideas (loops + if) with sensors.
  • "Drive forward until the color changes" โ€” the most common robot behavior.
  • Why while True: is your friend.
โ–ถ SPIKE Prime ยท Python
from hub import port
import runloop
import motor_pair
import color_sensor
import color

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def main():
    # Start the robot moving forward (no await โ€” keeps rolling)
    motor_pair.move(motor_pair.PAIR_1, 0, velocity=500)

    # Wait until the color sensor sees black, then continue
    await runloop.until(lambda: color_sensor.color(port.B) is color.BLACK)

    # Stop the motors
    motor_pair.stop(motor_pair.PAIR_1)

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • Color sensor on port.B.
  • A surface with a black line or patch somewhere ahead of the robot.
Process
  1. Start the drive motors going forward โ€” the robot is now rolling.
  2. runloop.until(...) repeatedly checks the lambda until it returns True. The lambda asks "is the color sensor seeing black right now?"
  3. The moment the sensor sees black, the await finishes and the next line runs.
  4. Stop the motors.
Expected output
Robot drives forward until the color sensor crosses a black line, then stops on the spot. Nothing prints to the console.

This is the classic "drive until you see something" pattern. runloop.until with a lambda is SPIKE Prime's way of waiting on a sensor โ€” much cleaner than a while-loop in your own code.

Try it yourself

Modify the program: drive until the distance sensor reads less than 100 mm (something is close). Then back up 10 cm and stop.

9

Build Your First Mission

Goal: Put it all together. Time: ~30 min.

  • Breaking a real mission into steps (the planning matters more than the code).
  • Why you write one function per step, not one giant function.
  • The classic FLL pattern: leave base โ†’ drive to mission โ†’ do thing โ†’ return.
โ–ถ SPIKE Prime ยท Python
from hub import port
import runloop
import motor
import motor_pair

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

# 20 cm of travel โ‰ˆ 411 degrees on the Driving Base
LEAVE_DISTANCE = 411

async def leave_base():
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, LEAVE_DISTANCE, 0, velocity=500)

async def turn_right_90():
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, 180, 100, velocity=300)

async def activate_mission():
    await motor.run_for_degrees(port.C, 180, 300)

async def return_to_base():
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, -LEAVE_DISTANCE, 0, velocity=500)

async def main():
    # The whole mission, written like a recipe
    await leave_base()
    await turn_right_90()
    await activate_mission()
    await return_to_base()

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • Arm motor (the mission mechanism) on port.C.
  • The robot starts in the FLL home base, lined up to leave.
Process
  1. Define four small async functions, one per step of the mission.
  2. main() calls each one with await, in order.
  3. runloop.run(main()) kicks off the whole sequence.
Expected output
Robot drives forward ~20 cm out of base, turns right ~90ยฐ, the arm spins half a turn to activate the mission, then the robot drives backward ~20 cm to return. Nothing prints to the console.

The win here: main() reads like the mission plan. If a step breaks, you fix just that one function โ€” the rest still works.

Try it yourself

Pick a real mission from the current FLL season. Sketch the path on paper first. Then write one function per step. Don't try to make it perfect on the first run โ€” make it run, then make it better.

10

Make It Reliable

Goal: Same result every run. That's what wins. Time: ~30 min.

  • Why "it worked once" isn't enough.
  • Using sensors to confirm position instead of trusting time/distance.
  • Reset routines โ€” squaring up against a wall, calibrating gyro at start.
  • Battery effects โ€” full battery vs low battery changes how fast you move.
  • Start from the same spot every time. Use a base jig.
  • Reset the gyro right before a turn โ€” drift adds up.
  • Use color sensors to confirm the robot is on the right line, not just "it should be by now."
  • Lower your motor velocity for precision moves. Slower = more reliable.
โ–ถ SPIKE Prime ยท Python
from hub import port, motion_sensor
import runloop
import motor_pair

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def reset_for_mission():
    # 1. Zero the gyro so "0 degrees" means "facing where I start"
    motion_sensor.reset_yaw(0)

    # 2. Nudge backward into the base wall to square up
    await motor_pair.move_for_degrees(motor_pair.PAIR_1, -120, 0, velocity=200)

    # 3. Small pause so sensors settle before we read them
    await runloop.sleep_ms(200)

async def main():
    await reset_for_mission()

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • The hub's built-in gyro (no port โ€” it lives inside the hub).
  • Robot is sitting in or near the FLL home base, with the back end pointing at the base wall.
Process
  1. Zero the gyro so whichever direction the robot is currently facing becomes "0ยฐ."
  2. Drive backward 120ยฐ of motor rotation (about 6 cm) so the back of the robot squares up against the base wall.
  3. Sleep 200 ms so sensor readings have time to settle before the mission starts.
Expected output
Robot eases backward briefly into the base wall. The gyro is now zeroed, so every turn in the upcoming mission measures from this starting direction. Nothing prints to the console.

Call await reset_for_mission() as the first line inside main() on every mission โ€” same starting state means the same result.

Try it yourself

Add reset_for_mission() to your Lesson 9 mission. Run it 10 times in a row and count how many succeed. Each failure is a clue โ€” fix the most common one first.

That's all ten.

You wrote real Python. You drove a real robot. That's a big deal.

Nobody's grading you. If a lesson didn't click, come back to it later. If you want more, the next section has a few directions โ€” but you don't owe anyone the next step.

Going further

Two patterns worth stealing.

If you want to keep going past the core ten, here are two patterns every serious FLL team eventually writes. They're optional โ€” but if you understand how they work, you'll understand almost any robot program you read.

โ˜…

Line Following

Goal: Keep the robot on a black line as it drives. Time: ~25 min.

A line follower doesn't try to drive straight down the middle of the line โ€” it wiggles along the edge. As long as one wheel sees black and the other sees white, the robot turns toward black. When it crosses the edge, it turns the other way. Repeat. The result looks like a robot that's tracing the edge of the line.

โ–ถ SPIKE Prime ยท Python
from hub import port
import runloop
import motor_pair
import color_sensor
import color

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def follow_line_for(zigzags):
    for _ in range(zigzags):
        # Curve one way until the sensor sees white
        motor_pair.move_tank(motor_pair.PAIR_1, 300, 100)
        await runloop.until(lambda: color_sensor.color(port.B) is color.WHITE)

        # Curve the other way until the sensor sees black again
        motor_pair.move_tank(motor_pair.PAIR_1, 100, 300)
        await runloop.until(lambda: color_sensor.color(port.B) is color.BLACK)

    motor_pair.stop(motor_pair.PAIR_1)

async def main():
    # Wiggle along the line edge 6 times, then stop
    await follow_line_for(6)

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • Color sensor on port.B, mounted so it can see the floor just in front of the robot.
  • A black line on a white surface (or vice versa).
  • Robot starts with the sensor over the black line.
Process
  1. Drive curving slightly right (left wheel faster) until the sensor crosses off the line into white.
  2. Drive curving slightly left (right wheel faster) until the sensor crosses back onto the black line.
  3. Repeat for the requested number of zigzags, then stop.
Expected output
Robot wiggles its way along the edge of a black line โ€” left, right, left, right โ€” for 6 cycles, then stops. The path looks zigzag-y from above.
Try it yourself

Change the velocity numbers (300 and 100) to 500 and 200. Does the robot wiggle wider, or narrower? What happens if you set the two velocities equal?

โ˜…

Gyro-Accurate Turns

Goal: Turn exactly 90ยฐ โ€” same result every time, full battery or low. Time: ~25 min.

Earlier, we turned by telling the motors to spin a specific number of degrees. That works okay โ€” but the actual turn angle drifts as the battery drains, the floor changes, or the robot wears in. A gyro turn doesn't care: it watches the hub's built-in motion sensor and stops the instant the robot has actually rotated to the target angle. Same result every time. This is the secret weapon of every consistent FLL team.

Note: motion_sensor.tilt_angles()[0] returns the yaw in decidegrees (tenths of a degree). So 900 means 90.0ยฐ, -450 means -45.0ยฐ.

โ–ถ SPIKE Prime ยท Python
from hub import port, motion_sensor
import runloop
import motor_pair

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def right_turn_90():
    motion_sensor.reset_yaw(0)
    # Steering 100 spins the robot clockwise in place
    motor_pair.move(motor_pair.PAIR_1, 100, velocity=100)
    # Wait until yaw drops to -900 (= -90.0 degrees)
    await runloop.until(lambda: motion_sensor.tilt_angles()[0] <= -900)
    motor_pair.stop(motor_pair.PAIR_1)

async def left_turn_90():
    motion_sensor.reset_yaw(0)
    # Steering -100 spins counterclockwise
    motor_pair.move(motor_pair.PAIR_1, -100, velocity=100)
    await runloop.until(lambda: motion_sensor.tilt_angles()[0] >= 900)
    motor_pair.stop(motor_pair.PAIR_1)

async def main():
    await right_turn_90()
    await runloop.sleep_ms(500)
    await left_turn_90()

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • The hub's built-in motion sensor (lives inside the hub โ€” no port needed).
  • A flat surface so the robot can rotate cleanly.
Process
  1. Zero the gyro so the starting direction counts as 0ยฐ.
  2. Start spinning in place toward the target direction.
  3. runloop.until watches the yaw reading. The instant it crosses the target, stop.
  4. Pause briefly, then do the opposite turn.
Expected output
Robot spins 90ยฐ clockwise (right), pauses, then spins 180ยฐ counterclockwise to end up at +90ยฐ from its original heading. Should land within a degree or two of perfect every single time.
Try it yourself

Write a function turn_to(target_decidegrees) that takes any target angle (positive = right, negative = left) and handles either direction automatically. Hint: check the sign of the target before deciding which steering to use.

โ˜…

Calibration โ€” start from a known state

Goal: Reset the gyro and motor counters so every mission starts the same way. Time: ~15 min.

Nothing in real robotics is perfect. The gyro drifts a tiny bit while the hub sits on the table. Motor position counters keep counting from wherever they were last time. If you don't reset these before a mission, you're starting from a slightly different state every run โ€” which is exactly why "it worked once" turns into "it never works the same way twice."

The fix is small: zero everything at the top of every mission. It's a 5-line habit that turns inconsistent runs into reliable ones.

โ–ถ SPIKE Prime ยท Python
from hub import port, motion_sensor
import runloop
import motor
import motor_pair

motor_pair.pair(motor_pair.PAIR_1, port.A, port.E)

async def calibrate():
    # 1. Zero the gyro โ€” "facing forward" now counts as 0 degrees
    motion_sensor.reset_yaw(0)

    # 2. Reset each motor's position counter to 0
    motor.reset_relative_position(port.A, 0)
    motor.reset_relative_position(port.E, 0)
    motor.reset_relative_position(port.C, 0)  # arm motor

    # 3. Tiny pause so sensors settle before we start reading them
    await runloop.sleep_ms(300)

    print("Calibrated. Gyro = 0, motor positions = 0.")

async def main():
    await calibrate()
    # ... your mission code goes here ...

runloop.run(main())
Inputs
  • Drive motors on port.A and port.E.
  • Arm motor on port.C (or whichever port your build uses).
  • The hub's built-in gyro.
Process
  1. motion_sensor.reset_yaw(0) โ€” tells the gyro "the direction I'm facing right now is 0ยฐ."
  2. motor.reset_relative_position(port, 0) on each motor โ€” wipes the running degree counter back to 0. Especially important if you use motor.relative_position later in your mission.
  3. A 300 ms pause so all the sensors settle into clean readings.
Expected output
Robot doesn't move. Gyro and motor counters all read 0. Console prints "Calibrated." Your mission can now assume a known starting state.
Try it yourself

Drop await calibrate() as the first line inside main() of your Lesson 9 mission. Run the mission 5 times in a row. Did the results get more consistent compared to running without calibration?

โ˜…

Logging โ€” watch your sensors over time

Goal: Print sensor readings as CSV so you can graph them or spot patterns. Time: ~15 min.

Sensor readings change all the time. A single reading tells you almost nothing โ€” but 50 readings in a row tell you whether the sensor is stable, drifting, or noisy. Logging is the simple habit of printing sensor values to the console as your program runs, so you can see the trend.

Format the lines like a CSV (step,color,distance) and you can paste the console output into Google Sheets to graph it. This is how FLL teams figure out why a mission fails on run 3 โ€” usually the sensor was telling them, they just weren't listening.

โ–ถ SPIKE Prime ยท Python
from hub import port, motion_sensor
import runloop
import color_sensor
import distance_sensor

async def log_sensors(samples, interval_ms):
    # CSV header โ€” paste the whole output into a spreadsheet later
    print("step,color,distance_mm,yaw_decideg")

    for i in range(samples):
        c = color_sensor.color(port.B)
        d = distance_sensor.distance(port.D)
        y = motion_sensor.tilt_angles()[0]
        print(i, ",", c, ",", d, ",", y)
        await runloop.sleep_ms(interval_ms)

async def main():
    # Take 40 readings, one every 250 ms โ€” total of 10 seconds
    await log_sensors(40, 250)
    print("=== Logging done ===")

runloop.run(main())
Inputs
  • Color sensor on port.B, distance sensor on port.D.
  • How many samples to take (40) and how long between them (250 ms = 0.25 sec).
  • Whatever the sensors are pointed at during the run.
Process
  1. Print a CSV header row so it's spreadsheet-ready.
  2. Loop 40 times: read each sensor, print one CSV row, sleep 250 ms.
  3. After 10 seconds total, stop.
Expected output

Like this, depending on what the sensors see:

step,color,distance_mm,yaw_decideg
0 , 10 , 145 , 0
1 , 10 , 147 , 1
2 , 10 , 144 , 0
3 , 0 , 145 , -1
...
Try it yourself

Run the log while slowly waving your hand in front of the distance sensor. Copy the console output, paste it into Google Sheets, split by comma, and make a line chart of the distance_mm column. You'll see the noise floor of the sensor when nothing moves, and the spike when your hand enters its view. That's instinct you can't get from one reading.

Reading guide

How to read any SPIKE Prime program.

Once you can read SPIKE Prime Python, every sample program in LEGO's official docs becomes practice material. Here's the anatomy of a tiny program, annotated.

1from hub import port, light_matriximport runloopimport motor_pairimport color_sensorimport color 2motor_pair.pair(motor_pair.PAIR_1, port.A, port.E) 3async def main(): # Drive forward until the sensor sees red, then stop. motor_pair.move(motor_pair.PAIR_1, 0, velocity=300) 4await runloop.until(lambda: color_sensor.color(port.B) is color.RED) motor_pair.stop(motor_pair.PAIR_1)  # Show a heart on the hub. await light_matrix.write("♥") 5runloop.run(main())
1
Imports. Every line names a module the program will use. hub gives you port labels and built-in hardware (gyro, light_matrix). runloop runs your async code. motor_pair, color_sensor, and color are the libraries for the parts you'll touch.
2
Setup, outside main. Pairing motors only needs to happen once when the program starts. Anything that's "one-time configuration" sits above main().
3
async def main(): is the entry point. Every SPIKE Prime program puts its real work inside this function. Lines that take time (drive, wait, sleep) get await in front. Lines that don't (read a sensor, set up a motor) don't.
4
runloop.until(lambda: ...) is the "wait until" pattern. The lambda is a tiny function that returns True or False; runloop.until keeps checking until it returns True, then continues. This is how SPIKE Prime waits on sensors without you writing your own loop.
5
runloop.run(main()) at the very bottom is the line that actually runs the program. Defining async def main() doesn't run it โ€” this line does. Almost every SPIKE Prime program ends with this exact line.

When you read a new sample

  1. Scan the imports first. They tell you which sensors and motors the program uses before you read a single line of logic.
  2. Find main(). Everything interesting happens here.
  3. Look for await lines. Each one is a real action (drive, turn, wait). Read those in order โ€” that's the story of the program.
  4. Ignore the boilerplate. runloop.run(main()) at the bottom is always the same. Don't get stuck on it.
Where to go from here

You finished. What now?

No pressure either way โ€” you've already done the hard part. Here are a few directions if you want to keep going.

Build more with LEGO's own lessons.

LEGO Education runs free "Training Camp" lessons that pick up where this leaves off โ€” bigger robots, more sensors, harder challenges. Same SPIKE app you're already using.

LEGO Training Camps →

Try a real FLL mission.

The current FLL Challenge season has a mat with real missions โ€” driving to a station, picking something up, scoring points. Apply Lesson 9's pattern (one function per step) to one of them. Win or lose, the practice is the point.

About FIRST LEGO League →

Go deeper into Python.

Module 1 covered the basics. Python has way more โ€” lists, dictionaries, classes, libraries. If the language clicked for you, the official Python tutorial is well-written.

Official Python tutorial →

If you want to talk to other kids doing robotics in Loudoun, drop us a line at contact@loudounrobotics.org. No commitment โ€” just say hi.

Outside resources we like