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.
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.
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.
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.
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.
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.
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.
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.
Diagnostic โ paste this in and see what's wrong
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).
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
-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.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.
Port scanner โ type your guess, the code checks it
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".
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
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.
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.
What you'll learn
print() function โ how to make Python say things.Try this code
print("Hello, robot!") print("My name is " + "Lucy") print(7 + 3)
"Hello, robot!"."My name is " and "Lucy" into one piece of text, then print it.7 + 3 and print the result.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.
Write three print() lines: your name, your favorite mission from FLL, and the answer to 12 * 8. Then run your program.
What you'll learn
int, float) and text (str) are different types.+ - * / % โ that last one is "remainder."Try this code
# 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")
robot_name = "Sparky"wheel_diameter_cm = 5.6laps = 3distance_per_lap = 3.14 ร 5.6 (about one wheel's circumference).laps to get total_distance.Sparky traveled 52.8 cm
Make a variable missions_scored set to 4 and points_per_mission set to 25. Print "Total points: ___" โ your program does the math.
What you'll learn
if, elif, and else let your program choose.== < > <= >= !=.Try this code
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")
battery = 42battery > 70 โ no, it's only 42, so skip.battery > 30 โ yes, 42 is greater than 30, so print the "okay" message.else branch never runs.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.
Make a variable color_seen. If it's "red", print "Stop!". If it's "green", print "Go!". Otherwise print "Wait for the right color."
What you'll learn
for loops โ repeat a fixed number of times.while loops โ repeat until something is true.range() โ Python's way of counting.Try this code
# 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))
range(5) gives you), print "Lap" plus that number + 1.points = 0.points is under 100, add 25 to it and print the running total.points hits 100, the while condition becomes false and the loop stops.Lap 1 Lap 2 Lap 3 Lap 4 Lap 5 Scored! Total: 25 Scored! Total: 50 Scored! Total: 75 Scored! Total: 100
Print the numbers 1 through 10, but for every number divisible by 3, print "fizz" instead. Hint: use % to get the remainder.
What you'll learn
define your own functions.Try this code
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))
mission_score(missions_completed, bonus) โ takes two numbers per call.(4, 10). Call 2: (6, 0).mission_score: multiplies missions_completed by 25 for the base, adds bonus, returns the total.mission_score(4, 10) โ 4 ร 25 + 10 = 110. Save to round_1.mission_score(6, 0) โ 6 ร 25 + 0 = 150. Save to round_2.Round 1: 110 Round 2: 150
Every robot mission in Module 2 will start by you writing a function. Get comfortable here.
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.
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.
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.
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.
What you'll learn
MotorPair โ driving two motors together as one robot.SPIKE Prime code
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())
port.A (left) and port.E (right) on the Driving Base.617 degrees at velocity 500 with steering 0 (straight).180 degrees at velocity 300 with steering 100 (sharp right).motor_pair.PAIR_1).runloop.run(main()) at the bottom is what actually starts the program.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.
What you'll learn
SPIKE Prime code
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())
port.B.port.D.color.RED using is. If it matches, print a follow-up.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.
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.
What you'll learn
while True: is your friend.SPIKE Prime code
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())
port.A and port.E.port.B.runloop.until(...) repeatedly checks the lambda until it returns True. The lambda asks "is the color sensor seeing black right now?"await finishes and the next line runs.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.
Modify the program: drive until the distance sensor reads less than 100 mm (something is close). Then back up 10 cm and stop.
What you'll learn
SPIKE Prime code
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())
port.A and port.E.port.C.main() calls each one with await, in order.runloop.run(main()) kicks off the whole sequence.The win here: main() reads like the mission plan. If a step breaks, you fix just that one function โ the rest still works.
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.
What you'll learn
Reliability tips
A reset routine you can drop into any mission
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())
port.A and port.E.Call await reset_for_mission() as the first line inside main() on every mission โ same starting state means the same result.
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.
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.
The idea
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 code
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())
port.A and port.E.port.B, mounted so it can see the floor just in front of the robot.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?
The idea
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 code
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())
port.A and port.E.runloop.until watches the yaw reading. The instant it crosses the target, stop.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.
The idea
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 code
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())
port.A and port.E.port.C (or whichever port your build uses).motion_sensor.reset_yaw(0) โ tells the gyro "the direction I'm facing right now is 0ยฐ."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.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?
The idea
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 code
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())
port.B, distance sensor on port.D.40) and how long between them (250 ms = 0.25 sec).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 ...
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.
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.
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.main().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.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.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
main(). Everything interesting happens here.await lines. Each one is a real action (drive, turn, wait). Read those in order โ that's the story of the program.runloop.run(main()) at the bottom is always the same. Don't get stuck on it.No pressure either way โ you've already done the hard part. Here are a few directions if you want to keep going.
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 →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 →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.