Data Structures, Modular Programming, and Version Control
Welcome, maker — let's organize your code like a pro!
You know variables and loops. Now we add containers that hold many values at once, patterns that keep your code tidy across multiple files, and a tool that saves every version of your work so you can always go back. These skills make the difference between a first draft and a finished project.
Summary
This chapter rounds out the core MicroPython toolkit and introduces software
engineering practices that scale. Students learn Python's primary data structures —
lists, tuples, and dictionaries — along with string manipulation and formatted output.
The chapter then teaches modular programming patterns: writing reusable functions,
separating hardware configuration into a config.py file, and keeping secrets like
WiFi credentials out of version control. Students also learn Git basics so they can
track their robot programs throughout the course.
Concepts Covered
This chapter covers the following 15 concepts from the learning graph:
- Lists
- List Indexing
- List Iteration
- Tuples
- Dictionaries
- String Manipulation
- Formatted Strings
- Modular Programming
- Reusable Functions
- Import Config Pattern
- Serial Communication
- Software Troubleshooting
- Code Documentation
- Version Control Git
- Git Commit Workflow
Prerequisites
This chapter builds on concepts from:
- Chapter 3: MicroPython and Development Environment Setup
- Chapter 4: Control Flow, Functions, and Exception Handling
Lists — Ordered Collections
A list is an ordered collection of values stored under a single variable name. Instead of creating color_0, color_1, color_2, you store all three in one list. Lists use square brackets and commas:
1 2 3 | |
Lists can hold any data type — strings, integers, floats, even other lists. A single list can even mix types, though it is usually clearer to keep one type per list.
List Indexing
Indexing means accessing a single item by its position. Python counts positions starting at 0, not 1. So the first item in colors is at index 0, the second at index 1, and so on.
1 2 3 4 5 | |
Negative indexes count from the end. colors[-1] gives "blue" — the last item. colors[-2] gives "green".
You can also update a list item by assigning to an index:
1 | |
Use len(colors) to find out how many items the list has. Accessing colors[3] when there are only 3 items (indexes 0–2) raises an IndexError — a very common bug.
List Iteration
Iterating a list means visiting each item one at a time. The simplest way uses a for loop:
1 2 | |
When you also need the index, use enumerate():
1 2 | |
Common list methods worth knowing:
colors.append("yellow")— adds an item to the endcolors.remove("green")— removes the first matching itemlen(colors)— returns the number of items
Diagram: List Index Explorer
Run List Index Explorer Fullscreen
Interactive MicroSim showing list indexing and iteration
Type: MicroSim
sim-id: list-index-explorer
Library: p5.js
Status: Specified
Create a p5.js MicroSim with a 700 × 300 canvas. Show a horizontal array of 5 colored boxes labeled with values (e.g., "red", "green", "blue", "orange", "purple"). Above each box, display the positive index (0, 1, 2, 3, 4). Below each box, display the negative index (-5, -4, -3, -2, -1).
Controls: - A slider "Index" from -5 to 4 selects a box, which highlights yellow with a glowing border. - A text display shows: "colors[X] = 'value'" where X is the current index. - A "Iterate" button animates the highlight moving left to right through all items with a 500ms delay between steps.
Clicking any box directly selects it and updates the index display.
Learning objective (Bloom's Taxonomy — Applying): students practice accessing specific items by index, including negative indexing.
Responsive: redraw on window resize.
Tuples — Immutable Sequences
A tuple is like a list, but it cannot be changed after creation. Tuples use parentheses instead of square brackets:
1 2 3 | |
The word immutable means "cannot be mutated (changed)." Once you create a tuple, you cannot append to it, remove from it, or change its values. Python enforces this. Trying to do motor_pins[0] = 5 raises a TypeError.
Tuples are useful when the data should never change — hardware pin numbers, screen dimensions, color constants. Using a tuple instead of a list signals to anyone reading your code: "these values are fixed on purpose."
Tuples support indexing and iteration exactly like lists:
1 2 3 4 | |
Diagram: Tuple vs List Mutability
This MicroSim puts a list and a tuple side by side and lets you try to change both. You see the list accept your change and the tuple refuse with a TypeError. It helps you choose the right container for robot data.
Run Tuple vs List Mutability Fullscreen
Try to change a list and a tuple and see which one allows it
Type: microsim
sim-id: tuple-list-mutability-explorer
Library: p5.js
Status: Specified
Reuse: computer-science / tuple-vs-list-mutability (https://github.com/dmccreary/computer-science/tree/main/docs/sims/tuple-vs-list-mutability). Keep the side-by-side mutation test. Replace the generic data with motor_pins, an RGB color, and the OLED board_size.
Learning objective: Distinguish (Bloom L2-L4) — the student can predict which operations work on a list and which raise a TypeError on a tuple, and pick the right one for fixed hardware values.
Canvas layout: 700 px wide (responsive), 420 px tall. Top strip (70 px): data set selector. Two equal panels below, left "List [ ]" and right "Tuple ( )", each 340 px wide and 250 px tall. Bottom strip (80 px): results console.
Visual elements: - Each panel shows its container as a row of boxes with the index above and value inside. Lists use blue boxes with square-bracket ends. Tuples use gray boxes with round-bracket ends and a small padlock icon above them. - Below each row, the Python code for the current attempt in monospace. - A green check bubble ("Worked!") or a red X bubble with the text "TypeError: 'tuple' object doesn't support item assignment" appears next to each panel after an attempt. - When a list change works, the changed box flashes yellow and the new value appears. When a tuple change fails, the padlock shakes for half a second.
Interactive controls: - Dropdown "Data set": "motor_pins = 6, 7, 8, 9" (default), "rgb_red = 255, 0, 0", "board_size = 128, 64". - Dropdown "Operation": "Change item 0 to 5" (default), "Append a value", "Remove the last item", "Read item 0", "Loop over all items". - "Try it on both" button. "Reset" button. - Text field "New value" (integer 0 to 255, default 5) used by the change and append operations.
Behavior:
- Read item 0 and Loop over all items work on both and print the same result.
- Change item 0, Append, and Remove work on the list only. The list changes on screen. The tuple stays the same and shows the TypeError (for Append or Remove the message is AttributeError: 'tuple' object has no attribute 'append').
- The results console counts "List: N changes worked, Tuple: N changes blocked" and keeps a running tally.
- A hint line names the rule: "Use a tuple when the values must never change, like pin numbers."
Default state: motor_pins data set loaded, "Change item 0 to 5" selected, no attempt made yet.
Assessment/Challenge: You need to store the two OLED dimensions (128, 64) and a list of the last ten distance readings. Which is a tuple and which is a list? Answer: the dimensions are a tuple because they never change, and the readings are a list because new readings keep being added. The student checks by trying "Append" on each data set.
Responsive: redraw on window resize.
Your config.py file from the next section stores pin numbers as fixed values. This sim shows why a tuple is a good choice for those values: nobody can change them by accident while the robot runs. Use a list for anything that grows, like a log of sensor readings.
Dictionaries — Key-Value Stores
A dictionary stores pairs of keys and values. Instead of accessing data by position (like a list), you access it by name. Dictionaries use curly braces:
1 2 3 4 5 6 | |
Access a value with its key in square brackets:
1 2 | |
Update a value the same way:
1 2 | |
Add a new key-value pair by simply assigning to a new key:
1 | |
Dictionaries are great for grouping related data about one thing. Instead of separate variables robot_name, robot_speed, robot_distance, you have one robot dictionary with all the properties in one place.
The table below compares the three data structures:
| Structure | Syntax | Access method | Mutable? | Best for |
|---|---|---|---|---|
| List | [a, b, c] |
Index [0] |
Yes | Ordered sequences, sensor logs |
| Tuple | (a, b, c) |
Index [0] |
No | Fixed values, pin assignments |
| Dictionary | {k: v} |
Key ["name"] |
Yes | Named properties of one object |
Which container should I use?
Ask yourself: "Do I need to change the values? Are the values named or numbered?" If the values are fixed, use a tuple. If they're numbered in order, use a list. If they're named properties of one thing (like a robot's stats), use a dictionary. Getting this right makes your code easier to read and harder to break.
Diagram: Sensor Dictionary Explorer
This MicroSim shows a robot dictionary as a set of labeled drawers. You look up a key, change a value, add a new key, and see what happens when a key does not exist. Each action shows the matching line of MicroPython.
Run Sensor Dictionary Explorer Fullscreen
Look up, update, and add keys in a robot dictionary
Type: microsim
sim-id: sensor-dictionary-explorer
Library: p5.js
Status: Specified
Reuse: learning-python / dictionary-key-lookup (https://github.com/dmccreary/learning-python/tree/main/docs/sims/dictionary-key-lookup) and computer-science / dictionary-structure (https://github.com/dmccreary/computer-science/tree/main/docs/sims/dictionary-structure). Keep the key-to-value drawer metaphor. Use the robot dictionary from this chapter (name, speed, is_moving, distance_cm) plus sensor readings.
Learning objective: Apply (Bloom L3) — the student can read, update, and add dictionary entries with keys and can predict a KeyError for a missing key.
Canvas layout: 700 px wide (responsive), 520 px tall. Left panel (400 px): the dictionary drawn as a cabinet of drawers. Right panel (rest): code and output. Bottom strip (90 px): controls.
Visual elements:
- Cabinet: one row per key. Each row has a colored key label tab on the left (teal, 14 px bold) and a value box on the right (white with a dark outline). Starting rows: "name" -> "Sparky", "speed" -> 75, "is_moving" -> True, "distance_cm" -> 30.5.
- A yellow pointer arrow slides from the code panel to the key tab that is being looked up. The value box glows green when read successfully.
- A red drawer with a lock icon appears for a missing key, with the label "KeyError: 'battery_pct'".
- A new row slides in from the bottom with a blue outline when a new key is added.
- Right panel: the line of MicroPython for the current action (monospace) and, below it, the printed output in a black console box.
- A toggle panel "Compare to a list" shows the same values as [ "Sparky", 75, True, 30.5 ] with indexes 0 to 3, so students see "which is easier to read: robot[1] or robot["speed"]".
Interactive controls: - Dropdown "Key": name, speed, is_moving, distance_cm, battery_pct (missing key). - Buttons: "Read", "Update value", "Add new key", "Reset". - Text field "New value" for Update and Add (default 50 for speed, 85 for battery_pct). - Checkbox "Compare to a list" (default off).
Behavior:
- Read: pointer moves to the key, the value is printed, e.g. print(robot["speed"]) prints 75. For a missing key, the KeyError drawer appears and the output shows the error line.
- Update value: changes the value box (flash yellow) with robot["speed"] = 50.
- Add new key: for a key that is not in the dictionary, robot["battery_pct"] = 85 adds a row. Adding an existing key just updates it.
- The number of keys is shown as len(robot) in the corner and updates live.
- Reset restores the four starting rows.
Default state: four rows shown, "name" selected, code panel shows print(robot["name"]), output empty.
Assessment/Challenge: Read battery_pct first and get the KeyError. Then fix the error without changing the code line. Answer: use "Add new key" to set battery_pct to 85, then read it again and see 85.
Responsive: redraw on window resize.
Robot programs are full of named values like speed, distance, and battery level. A dictionary keeps them together in one place, so robot["speed"] reads like plain English. Later chapters use dictionaries to hold sensor readings and settings, so try the KeyError case here where it is safe.
String Manipulation
Strings are more than just labels. MicroPython provides many operations for working with text — splitting it, joining it, searching it, and formatting it.
Some useful string operations:
1 2 3 4 5 6 7 8 | |
String slicing lets you extract a part of a string. message[0:5] gives characters at indexes 0, 1, 2, 3, 4 — but not index 5. The format is [start:stop] where stop is not included.
Formatted Strings
Formatted strings (also called f-strings) let you embed variable values directly inside a string. Before the code, here is the idea: put the letter f before the opening quote, then use curly braces {} around any variable or expression you want to insert.
1 2 3 4 5 6 7 8 | |
The : inside {} adds formatting. :.1f means "one decimal place as a float." F-strings are the clearest way to build display text for the OLED screen.
Modular Programming
As your robot programs grow, one long main.py file becomes hard to manage. Modular programming means splitting your code into separate files — each file handles one concern. This is the same decomposition idea from Chapter 1, applied to code organization.
In MicroPython, each .py file is a module. You import it just like a built-in library.
The config.py Pattern
The most important modular pattern in this course is the config.py file. Hardware pin numbers belong in config.py, not scattered through your program. If you ever change which pin a motor connects to, you change one line in config.py and everything else still works.
Create config.py on your board with pin assignments as constants:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Then import it in main.py:
1 2 3 4 | |
This pattern keeps hardware details in one place and logic in another. Professional engineers call this separation of concerns — each file has exactly one job.
UPPERCASE constants are a signal
Pin numbers and hardware constants go in config.py in UPPERCASE. That capitalization signals to any reader: "this value is fixed — don't change it at runtime." When you see config.RIGHT_FORWARD_PIN in code, you know exactly where to look if the pin ever needs changing.
Reusable Functions and Module Files
You can also put groups of related functions in their own module file. For example, a motors.py file could hold all motor control functions:
1 2 3 4 5 6 7 | |
Then import and use it in main.py:
1 2 | |
This is exactly how the course's library files (vl53l0x.py, ssd1306.py) work. They are modules you import to get sensor and display functionality without writing those drivers yourself.
Diagram: Module Import Flow
This MicroSim shows three files on the robot: config.py, motors.py, and main.py. You change one pin number in config.py and watch the change flow into the other two files through import config. It shows why the config.py pattern saves you time.
Run Module Import Flow Fullscreen
Edit one pin in config.py and watch it flow through the imports
Type: microsim
sim-id: module-import-flow
Library: p5.js
Status: Specified
Reuse: None — new design
Learning objective: Explain (Bloom L2-L3) — the student can explain how import links files together and can predict which files change behavior when one value in config.py changes.
Canvas layout: 700 px wide (responsive), 500 px tall. Three file cards arranged in a triangle: config.py at the top center, motors.py bottom left, main.py bottom right. A strip on the left shows a small robot picture. A console box (700 x 100 px) sits along the bottom.
Visual elements:
- Each file card is a rounded rectangle (200 x 130 px) with a title tab and 3 to 5 lines of code in monospace 12 px. config.py is blue, motors.py is green, main.py is orange.
- config.py shows RIGHT_FORWARD_PIN = 11, RIGHT_REVERSE_PIN = 10, LEFT_FORWARD_PIN = 9, LEFT_REVERSE_PIN = 8.
- motors.py starts with import config and uses config.RIGHT_FORWARD_PIN in a line such as right_fwd = Pin(config.RIGHT_FORWARD_PIN).
- main.py starts with import config and import motors, then calls motors.stop_all().
- Arrows: motors.py to config.py and main.py to config.py labeled "import config". main.py to motors.py labeled "import motors". Arrows pulse once when the import runs.
- Wherever config.RIGHT_FORWARD_PIN appears in a file, it is highlighted in yellow and shows the current value in a small badge.
- A tiny robot picture with four labeled wire pins (GP8, GP9, GP10, GP11) shows which pin the right forward motor wire is plugged into.
Interactive controls: - Dropdown "RIGHT_FORWARD_PIN" with choices 11 (default), 12, 13, 14, 15 (the pin used in config.py). - Toggle "Hard-code pins instead (no config.py)" (default off). - "Run main.py" button plays the import order with animation. - "Reset" button.
Behavior: - On "Run main.py", the console prints in order: "main.py: import config", "config.py loaded", "main.py: import motors", "motors.py: import config (already loaded)", "Right forward pin: 11" (or the current value). - Changing the pin dropdown edits only the one line in config.py. All yellow-highlighted spots in main.py and motors.py update at once and the wire on the robot picture moves to the new GP pin. The console shows "1 line changed, 0 other files edited". - When "Hard-code pins instead" is on, the pin number appears typed as a raw number in 5 places across main.py and motors.py. Changing the dropdown then only updates the config.py line, and the other places stay at 11. The console shows "Pin mismatch! motors.py still uses 11" and the robot picture shows the motor wire not connected. - The counter "Lines to edit: 1" (with config.py) or "Lines to edit: 5" (hard-coded) is always shown.
Default state: config.py pattern on, pin 11, nothing run yet.
Assessment/Challenge: You move the right forward motor wire from pin 11 to pin 13. How many lines must you edit with the config.py pattern and how many with hard-coded pins? Answer: 1 line with config.py, 5 lines when hard-coded (the student checks the "Lines to edit" counter).
Responsive: redraw on window resize.
This is why every kit in this course has a config.py file. When you rewire a motor, you change one line and every file that imports config follows along. The same idea lets main.py use motors.stop_all() without knowing how the motors work inside.
Serial Communication
Serial communication is how MicroPython sends text to Thonny's Shell pane. Every print() statement you write sends data over the USB cable as serial output. This works because the USB connection creates a virtual serial port — a communication channel that sends data one bit at a time.
Serial output is your most basic debugging tool. When you don't know what a sensor is reading, add a print(). When you don't know if a function is being called, add a print(). This technique is called print debugging, and professional engineers use it constantly.
1 2 3 4 5 | |
Remove or comment out debug prints before your final program — too many slows the loop slightly and clutters the output.
Software Troubleshooting
Software troubleshooting is the systematic process of finding and fixing bugs. Bugs are not failures — they are information. An error message tells you exactly where something went wrong and what type of error occurred.
When Thonny shows a red traceback, read it from the bottom up:
- The last line names the exception type and message:
AttributeError: 'NoneType' object has no attribute 'read' - The line above shows the file and line number:
File "main.py", line 23 - Working upward shows the call stack — how the program got there
A systematic troubleshooting process:
- Read the full error message before changing anything.
- Identify the file and line number from the traceback.
- Add print statements before and after the failing line.
- Check one variable at a time in the REPL.
- Test the smallest possible piece of code that still shows the bug.
This process — isolate, hypothesize, test, observe — is the engineering design process applied to code.
Code Documentation
Code documentation means writing explanations that help readers understand your code. There are two places to do this in Python: comments and docstrings.
Comments use # and explain a single line or short section. You practiced these in Chapter 3.
Docstrings are strings at the top of a function that explain what it does. They use triple quotes:
1 2 3 4 5 | |
Good documentation answers: What does this do? What inputs does it need? What does it return? These questions matter most when someone else reads your code — or when you return to it after a week away.
Version Control with Git
Version control is a system that tracks changes to your files over time. It lets you save checkpoints of your code. If you break something, you can go back to the last working checkpoint. We use Git — the most widely used version control system in the world.
Git stores your code's history in a hidden folder called a repository (or repo). Every checkpoint is called a commit.
Initializing a Repository
On your laptop (not the robot board), open a terminal in your project folder. Before the commands below, here is what each does: git init creates a new repository. git add stages files for the next commit. git commit saves those files as a checkpoint with a description.
1 2 3 | |
The Git Commit Workflow
The Git commit workflow is the cycle you repeat after every meaningful change:
- Make changes to your code in Thonny.
- Test the changes on the robot.
- Copy the updated files to your laptop.
- Stage the changed files:
git add filename.py - Commit with a clear message:
git commit -m "Add collision avoidance function"
A good commit message completes the sentence "This commit will...". "Fix motor speed bug" is good. "updates" is not.
Diagram: Git Commit Workflow
Run Git Commit Workflow Fullscreen
Interactive diagram of the Git commit workflow stages
Type: diagram
sim-id: git-commit-workflow
Library: Mermaid
Status: Specified
Create a Mermaid flowchart (graph LR, left to right) showing the four Git areas:
- "Working Directory" (yellow box) — where you edit files
- "Staging Area" (orange box) — files marked for the next commit
- "Local Repository" (blue box) — committed history on your laptop
- "Remote (GitHub)" (green box) — optional cloud backup
Arrows between areas labeled with commands:
- Working Dir → Staging Area: git add
- Staging Area → Local Repo: git commit -m "message"
- Local Repo → Remote: git push
- Remote → Local Repo: git pull
Every box and every arrow has a click directive that opens an infobox with a plain-language explanation of what that area stores or what that command does.
Canvas: 700 × 200 px. Responsive on window resize.
The .gitignore File
Not every file belongs in version control. A .gitignore file lists patterns of files Git should ignore.
Create a file named .gitignore in your project folder:
1 2 3 4 5 6 7 8 9 10 | |
The secrets.py file stores your WiFi network name and password. If you commit it to a public repository, anyone in the world can see your credentials. Always list secrets.py in .gitignore before your first commit.
Never commit secrets.py
WiFi passwords and API keys should never be in a Git repository. Add secrets.py to .gitignore before your first commit. If you accidentally commit credentials, consider them compromised — change the password immediately. This is a real-world security principle, not just a class rule.
Diagram: What Goes in Git Sorting Activity
This MicroSim is a sorting game. You drag project files into two bins: "Commit to Git" and "Put in .gitignore". The sim checks your answers and shows what is at risk when a secret file ends up in the wrong bin.
Run What Goes in Git Sorting Activity Fullscreen
Drag robot project files into the commit or ignore bin
Type: microsim
sim-id: git-what-to-commit-sorter
Library: p5.js
Status: Specified
Reuse: None — new design
Learning objective: Classify (Bloom L2-L4) — the student can decide which robot project files belong in a Git repository and which belong in .gitignore, and can explain why.
Canvas layout: 700 px wide (responsive), 480 px tall. Left column (260 px): a pile of file cards. Right side: two large bins side by side, "Commit to Git" (green outline) and "Put in .gitignore" (red outline), each 200 px wide and 300 px tall. Bottom strip (80 px): feedback message and buttons.
Visual elements:
- Ten file cards, each a small rounded rectangle with a file icon and file name in monospace: main.py, config.py, motors.py, vl53l0x.py, secrets.py, __pycache__/, notes.pyc, .DS_Store, README.md, wifi_password.txt.
- Cards are draggable. A card snaps into a bin when dropped over it and returns to the pile if dropped elsewhere.
- After "Check answers", correct cards get a green check and wrong cards get a red X and shake. Clicking a wrong card shows a one-sentence reason in the feedback strip.
- A "Repository preview" box under the commit bin lists what would be public on GitHub. If secrets.py or wifi_password.txt is in the commit bin, this box turns red with a lock icon and the words "Your WiFi password is now public!".
Interactive controls: - Drag and drop for each card. - "Check answers" button, "Reset" button. - Toggle "Show hints" (default off): adds a one-line hint under each card name, for example "Your code" or "Holds your WiFi password".
Behavior:
- Correct answers: Commit: main.py, config.py, motors.py, vl53l0x.py, README.md. Ignore: secrets.py, __pycache__/, notes.pyc, .DS_Store, wifi_password.txt.
- Reasons shown on wrong cards: secrets.py and wifi_password.txt — "holds passwords; anyone can read a public repo". __pycache__/ and notes.pyc — "made automatically; can be rebuilt". .DS_Store — "junk file from macOS". vl53l0x.py — "a library your robot needs, so others need it too".
- A score shows "N of 10 correct". A "Generated .gitignore" panel builds live from the ignore bin, one line per file name, so students see the file they would create.
- The "Repository preview" updates every drop, before checking.
Default state: all ten cards in the pile, both bins empty, score hidden.
Assessment/Challenge: Sort all ten cards with no hints and get a score of 10 of 10. Then answer: what happens if you commit secrets.py before adding it to .gitignore? Answer: the password is stored in the repository history, so the student should change the password right away.
Responsive: redraw on window resize.
The .gitignore file you just built in the sim is the same one from this chapter. Use this checklist before your first commit: code and docs go in, passwords and auto-made files stay out. It takes one minute and protects your WiFi network.
Key Takeaways
- Lists store ordered, changeable sequences accessed by index (
[0],[-1]) - Tuples store fixed, unchangeable sequences — use them for pin numbers and constants
- Dictionaries store named key-value pairs — perfect for grouping an object's properties
- f-strings embed variables in text cleanly:
f"Speed: {speed_pct}%" - config.py centralizes all pin assignments — change hardware in one place
- Modular programming splits code across multiple files, each with one job
- Serial output (
print()) is your primary debugging tool - Git tracks code history; commit often with clear messages
- secrets.py never goes in version control — always add it to
.gitignore
You code like a software engineer now!
Double thumbs-up, maker! Lists, tuples, dictionaries, modular files, Git commits — you now have the tools that professional engineers use every day. The next chapter dives into the electronics and motors that make me actually move. Get ready to roll!