This guide covers a variety of development practices for CircuitPython core and library APIs. These APIs are both built-into CircuitPython and those that are distributed on GitHub and in the Adafruit and Community bundles. Consistency with these practices ensures that beginners can learn a pattern once and apply it throughout the CircuitPython ecosystem.
Adafruit funded libraries should be under the
adafruit organization and have the format
Adafruit_CircuitPython_<name> and have a corresponding
directory (aka package) or
adafruit_<name>.py file (aka module).
If the name would normally have a space, such as “Thermal Printer”, use an underscore instead
(“Thermal_Printer”). This underscore will be used everywhere even when the separation between
“adafruit” and “circuitpython” is done with a
-. Use the underscore in the cookiecutter prompts.
Community created libraries should have the repo format
not have the
adafruit_ module or package prefix.
Both should have the CircuitPython repository topic on GitHub.
As our Code of Conduct states, we strive to use “welcoming and inclusive language.” Whether it is in documentation or in code, the words we use matter. This means we disfavor language that due to historical and social context can make community members and potential community members feel unwelcome.
There are specific terms to avoid except where technical limitations require it. While specific cases may call for other terms, consider using these suggested terms first:
Note that “technical limitations” refers e.g., to the situation where an upstream library or URL has to contain those substrings in order to work. However, when it comes to documentation and the names of parameters and properties in CircuitPython, we will use alternate terms even if this breaks tradition with past practice.
Lifetime and ContextManagers¶
A driver should be initialized and ready to use after construction. If the
device requires deinitialization, then provide it through
deinit() and also
__exit__ to create a context manager usable with
For example, a user can then use
import digitalio import board import time led = digitalio.DigitalInOut(board.D13) led.direction = digitalio.Direction.OUTPUT for i in range(10): led.value = True time.sleep(0.5) led.value = False time.sleep(0.5) led.deinit()
This will deinit the underlying hardware at the end of the program as long as no exceptions occur.
Alternatively, using a
with statement ensures that the hardware is deinitialized:
import digitalio import board import time with digitalio.DigitalInOut(board.D13) as led: led.direction = digitalio.Direction.OUTPUT for i in range(10): led.value = True time.sleep(0.5) led.value = False time.sleep(0.5)
with statement ensures that the deinit code is run regardless of
whether the code within the with statement executes without exceptions.
For small programs like the examples this isn’t a major concern because all user usable hardware is reset after programs are run or the REPL is run. However, for more complex programs that may use hardware intermittently and may also handle exceptions on their own, deinitializing the hardware using a with statement will ensure hardware isn’t enabled longer than needed.
Verify your device¶
Whenever possible, make sure device you are talking to is the device you expect. If not, raise a RuntimeError. Beware that I2C addresses can be identical on different devices so read registers you know to make sure they match your expectation. Validating this upfront will help catch mistakes.
When designing a driver for a device, use properties for device state and use methods for sequences of abstract actions that the device performs. State is a property of the device as a whole that exists regardless of what the code is doing. This includes things like temperature, time, sound, light and the state of a switch. For a more complete list see the sensor properties bullet below.
Another way to separate state from actions is that state is usually something the user can sense themselves by sight or feel for example. Actions are something the user can watch. The device does this and then this.
Making this separation clear to the user will help beginners understand when to use what.
Here is more info on properties from Python.
Design for compatibility with CPython¶
CircuitPython is aimed to be one’s first experience with code. It will be the first step into the world of hardware and software. To ease one’s exploration out from this first step, make sure that functionality shared with CPython shares the same API. It doesn’t need to be the full API it can be a subset. However, do not add non-CPython APIs to the same modules. Instead, use separate non-CPython modules to add extra functionality. By distinguishing API boundaries at modules you increase the likelihood that incorrect expectations are found on import and not randomly during runtime.
When adding a new module for additional functionality related to a CPython module do NOT simply prefix it with u. This is not a large enough differentiation from CPython. This is the MicroPython convention and they use u* modules interchangeably with the CPython name. This is confusing. Instead, think up a new name that is related to the extra functionality you are adding.
For example, storage mounting and unmounting related functions were moved from
uos into a new
storage module. Terminal related functions were moved into
multiterminal. These names better match their functionality and do not
conflict with CPython names. Make sure to check that you don’t conflict with
CPython libraries too. That way we can port the API to CPython in the future.
When adding extra functionality to CircuitPython to mimic what a normal
operating system would do, either copy an existing CPython API (for example file
writing) or create a separate module to achieve what you want. For example,
mounting and unmount drives is not a part of CPython so it should be done in a
module, such as a new
storage module, that is only available in CircuitPython.
That way when someone moves the code to CPython they know what parts need to be
Whenever possible, document your code right next to the code that implements it. This makes it more likely to stay up to date with the implementation itself. Use Sphinx’s automodule to format these all nicely in ReadTheDocs. The cookiecutter helps set these up.
Use Sphinx flavor rST for markup.
Lots of documentation is a good thing but it can take a lot of space. To minimize the space used on disk and on load, distribute the library as both .py and .mpy, MicroPython and CircuitPython’s bytecode format that omits comments.
After the license comment:
""" `<module name>` - <Short description> ================================================= <Longer description.> """
At the class level document what class does and how to initialize it:
class DS3231: """DS3231 real-time clock. :param ~busio.I2C i2c_bus: The I2C bus the DS3231 is connected to. :param int address: The I2C address of the device. """ def __init__(self, i2c_bus, address=0x40): self._i2c = i2c_bus
Attributes are state on objects. (See Getters/Setters above for more discussion about when to use them.) They can be defined internally in a number of different ways. Each approach is enumerated below with an explanation of where the comment goes.
Regardless of how the attribute is implemented, it should have a short
description of what state it represents including the type, possible values and/or
units. It should be marked as
(write-only) at the end of
the first line for attributes that are not both readable and writable.
Comment comes from after the assignment:
def __init__(self, drive_mode): self.drive_mode = drive_mode """ The pin drive mode. One of: - `digitalio.DriveMode.PUSH_PULL` - `digitalio.DriveMode.OPEN_DRAIN` """
Comment comes from the getter:
@property def datetime(self): """The current date and time as a `time.struct_time`.""" return self.datetime_register @datetime.setter def datetime(self, value): pass
The current date and time as a
@property def temperature(self): """ The current temperature in degrees Celsius. (read-only) The device may require calibration to get accurate readings. """ return self._read(TEMPERATURE)
The current temperature in degrees Celsius. (read-only)
The device may require calibration to get accurate readings.
Data descriptor description¶
Comment is after the definition:
lost_power = i2c_bit.RWBit(0x0f, 7) """True if the device has lost power since the time was set."""
True if the device has lost power since the time was set.
BusDevice is an awesome foundational library that manages talking on a shared I2C or SPI device for you. The devices manage locking which ensures that a transfer is done as a single unit despite CircuitPython internals and, in the future, other Python threads. For I2C, the device also manages the device address. The SPI device, manages baudrate settings, chip select line and extra post-transaction clock cycles.
from adafruit_bus_device import i2c_device DEVICE_DEFAULT_I2C_ADDR = 0x42 class Widget: """A generic widget.""" def __init__(self, i2c, address=DEVICE_DEFAULT_I2C_ADDR): self.i2c_device = i2c_device.I2CDevice(i2c, address) self.buf = bytearray(1) @property def register(self): """Widget's one register.""" with self.i2c_device as i2c: i2c.writeto(b'0x00') i2c.readfrom_into(self.buf) return self.buf
from adafruit_bus_device import spi_device class SPIWidget: """A generic widget with a weird baudrate.""" def __init__(self, spi, chip_select): # chip_select is a pin reference such as board.D10. self.spi_device = spi_device.SPIDevice(spi, chip_select, baudrate=12345) self.buf = bytearray(1) @property def register(self): """Widget's one register.""" with self.spi_device as spi: spi.write(b'0x00') i2c.readinto(self.buf) return self.buf
When writing a driver, take in objects that provide the functionality you need rather than taking their arguments and constructing them yourself or subclassing a parent class with functionality. This technique is known as composition and leads to code that is more flexible and testable than traditional inheritance.
Wikipedia has more information on “dependency inversion”.
For example, if you are writing a driver for an I2C device, then take in an I2C object instead of the pins themselves. This allows the calling code to provide any object with the appropriate methods such as an I2C expansion board.
Another example is to expect a
DigitalInOut for a pin to
toggle instead of a
board. Taking in the
Pin object alone would limit the driver to pins on
the actual microcontroller instead of pins provided by another driver such as an
Lots of small modules¶
CircuitPython boards tend to have a small amount of internal flash and a small amount of ram but large amounts of external flash for the file system. So, create many small libraries that can be loaded as needed instead of one large file that does everything.
Speed isn’t as important as API clarity and code size. So, prefer simple APIs like properties for state even if it sacrifices a bit of speed.
Avoid allocations in drivers¶
Although Python doesn’t require managing memory, its still a good practice for
library writers to think about memory allocations. Avoid them in drivers if
you can because you never know how much something will be called. Fewer
allocations means less time spent cleaning up. So, where you can, prefer
bytearray buffers that are created in
__init__ and used throughout the
object with methods that read or write into the buffer instead of creating new
objects. Unified hardware API classes such as
busio.SPI are design to read and
write to subsections of buffers.
Its ok to allocate an object to return to the user. Just beware of causing more than one allocation per call due to internal logic.
However, this is a memory tradeoff so do not do it for large or rarely used buffers.
Use of MicroPython
const() feature, as discussed in this forum post, and in this issue thread, provides some
optimizations that can be useful on smaller, memory constrained devices. However,
const(), keep in mind these general guide lines:
- Always use via an import, ex:
from micropython import const
- Limit use to global (module level) variables only.
- If user will not need access to variable, prefix name with a leading
Sensor properties and units¶
The Adafruit Unified Sensor Driver Arduino library has a great list of measurements and their units. Use the same ones including the property name itself so that drivers can be used interchangeably when they have the same properties.
|Property name||Python type||Units|
||(float, float, float)||x, y, z meter per second per second|
||(float, float, float)||x, y, z micro-Tesla (uT)|
||(float, float, float)||x, y, z degrees|
||(float, float, float)||x, y, z radians per second|
||float||equivalent CO2 in ppm|
||float||Total Volatile Organic Compounds in ppb|
||int||non-unit-specifc proximity values (monotonic but not actual distance)|
||float||non-unit-specific light levels (should be monotonic but is not lux)|
||int||RGB, eight bits per channel (0xff0000 is red)|
||(time.struct, str)||Sample alarm time and string to characterize frequency such as “hourly”|
||time.struct||date and time|
||int||16-bit PWM duty cycle (regardless of output resolution)|
||int||16-bit Analog value, unit-less|
||float||non-unit-specific sound level (monotonic but not actual decibels)|
Adding native modules¶
The Python API for a new module should be defined and documented in
shared-bindings and define an underlying C API. If the implementation is
port-agnostic or relies on underlying APIs of another module, the code should
shared-module. If it is port specific then it should live in
within the port’s folder. In either case, the file and folder structure should
mimic the structure in
To test your native modules or core enhancements, follow these Adafruit Learning Guides for building local firmware to flash onto your device(s):
Keeping compatibility with MicroPython isn’t a high priority. It should be done when its not in conflict with any of the above goals.