The print() function is your first debugging tool
When creating a CircuitPython program a great way to follow program flow and to examine parameters and values is to use a print() statement sending the information to the REPL. This is simple, tried and true, and you can comment them out when no longer needed to turn them off. Many times this is the way to go.
Sometimes, however, you might have a lot of print statements and multiple function calls and all of the information flowing into the REPL becomes a bit hard to follow.
Making print() output easier to read
At this point one strategy to make the print outputs easier to follow is to add divider lines, line feeds, special characters, etc. For example:
print("--------------------------------------------") # prints the dashes to the REPL
print("-"*40) # prints 40 dashes to the REPL more efficiently
print("\n") # executes a line feed in the REPL
print(" Here is an indented statement") # Add spaces to add indentions - use 2 spaces, 4 spaces, etc. for different indention levels
print("> Adds a greater than symbol") # Adding a special character makes certain lines stand out
Get creative and use different characters for divider lines (i.e. =, +, *). You can also combine characters such as:
print("-="*10) # Prints '-=-=-=-=-=-=-=-=-=-='
print("+ "*5) # Prints '+ + + + + '
When print() isn't enough - Turning it up to 11
In 99% of your projects these approaches will be just what you need. But then... what if your program has grown to almost 5000 lines of code, more than 40 functions and a really complex program flow. The print statements whiz by in the REPL at a dizzying speed. Is there a better way? If you have an SD card (or other writable partition) in your project, then there is!
I recently ran into this on a Fruit Jam project. Tons of memory means it supports really large programs and can process lots of data. I wanted to monitor the memory usage of different parts and check the timing as the program progresses. I found the best way to do this is to log everything to the SD card, so I created a simple function I named log():
def log(msg):
gc.collect()
t = time.monotonic()
m = gc.mem_free()
pt = rtc.RTC().datetime
txt = f"{pt[0]}{pt[1]:02d}{pt[2]:02d}{pt[3]:02d}{pt[4]:02d}{pt[5]:02d},{t:.2f},{m},{msg}\n"
try:
time.sleep(.1)
with open("/sd/runlog.csv", "a") as f:
f.write(txt)
except Exception as e:
print("Error writing to runlog file:", e)
print(txt)
Function explanation step-by-step
-
- Calls
gc.collect()to clean up memory - Captures system time with
time.monotonic() - Captures available memory with
gc.mem_free() - Retrieves real-time clock values via
rtc.RTC().datetime - Formats all data into a CSV-friendly string
- Appends the log entry to
/sd/runlog.csv - Prints the same entry to the REPL for live feedback
- Calls
Use is straightforward: instead of calling print() function substitute log(). You put the message you want to send to the REPL in quotes just like a print statement. You can also use f-strings and/or concatenate multiple items to send just like you wanted to print it out.
The log() function will start by doing a garbage collection, gets system time and free memory (two good things to track in complex programs). In the project I set and use the RTC, so I also pull the real time and format is as a simple number - in essence this is an ever-increasing serial number. This is useful as system time resets each time the processor is reset. Then this is all appended to 'runlog.csv' on the SD card. As a bonus it also prints it to the REPL so you still have the REPL output for most troubleshooting.
Here is some sample output from my project:
20251102130611,5140.08,6485552,Page 10 wind speed graphics updated 20251102130613,5141.49,6485504,> Page 8 update time: 88.563 seconds 20251102130614,5142.89,6488368,> Through page 8 update took 161.38 seconds 20251102130617,5145.71,6487856,History saved: 11/02/2025 1:03 PM 20251102130618,5147.11,6488432,> Through history data save took 165.60 seconds 20251102130624,5152.96,6476032,History purged: kept 94 of 94 records 20251102130625,5154.36,6488272,> Through old history purge took 172.85 seconds
The big advantage to this approach is that you can then open the output in a spreadsheet and analyze program flow. By calculating the time difference in seconds in the system time column you can see how long each step between calls takes. Likewise, you can track the memory usage step by step, all in your spreadsheet rather than calculating that in the program flow itself.
Of course, this adds some overhead to your program as well. Once you have completed debugging and everything does what you want, simply comment out the code in the log() function and add a pass statement - then you can always turn it back on if you need it again.
Whether you're just starting out or managing a complex CircuitPython project, these logging strategies can help you stay in control of your program flow. Start simple with print(), enhance readability with formatting tricks, and scale up with SD card logging when your project demands it.
This page (Troubleshooting CircuitPython Programs - Beyond the Print Statement) was last updated on February 06, 2026.