The previous page, Communicating Between Tasks, showed how to handle button presses while blinking LEDs at the same time. A button press is an example of an asynchronous event, an event caused by something outside the running program, that can happen at any time.


Microcontrollers provide a hardware mechanism called interrupts for handling asynchronous events. A hardware interrupt can be generated when a pin changes state, when an internal timer triggers, when some hardware operation has completed, such as an I2C read or write, or for numerous other reasons. These events are usually asynchronous to the program being run, though sometimes interrupts are used to indicate that the program has caused an error, such as accessing a non-existent memory address.

When an interrupt occurs, the interrupt mechanism will call a routine called an interrupt handler. The currently running program is temporarily suspended and other interrupts of lower priority are blocked. The interrupt handler routine does something quickly and returns, and then the regular program (usually) resumes. An interrupt handler is an example of preemptive multitasking, which was mentioned in the multitasking Overview page.

For example, a pin connected to an external sensor may change, indicating that the sensor has new data. The interrupt handler itself could read the data and record it, but often the sensor would take too long to respond. So instead of reading the sensor data directly in the interrupt handler, the handler would set a flag to indicate that new data is available. Or, it may schedule a task to run later to read that data. Checking the flag or running the task occurs later, often inside an event loop.

The actual hardware interrupt is often called a hard interrupt, because it's generated and handled by the hardware. Acting on that interrupt later in an asynchronous fashion via software is often called handling a soft interrupt.


The alternative to interrupts is polling. When you check something over and over, waiting for a change, you are polling. For instance, you can monitor a DigitalInOut.value over and over in a loop. In the examples in this guide, you'll see a number of cases where some code checks for a condition, and then does an asyncio.sleep(). The code is polling, but in a controlled way, so that it doesn't block other code from running.

Handling Interrupts with countio

CircuitPython provides countio, a native module that counts rising-edge and/or falling-edge pin transitions. Internally, countio uses interrupts or other hardware mechanisms to catch these transitions and increment a count.

You can use countio with asyncio to catch interrupts and do something based on that interrupt. Here is a simple example using countio to monitor a pin connected to a push button, which will simulate a device interrupt. Note that the countio value is being polled in the task.

import asyncio
import board
import countio

async def catch_interrupt(pin):
    """Print a message when pin goes low."""
    with countio.Counter(pin) as interrupt:
        while True:
            if interrupt.count > 0:
                interrupt.count = 0
            # Let another task run.
            await asyncio.sleep(0)

async def main():
    interrupt_task = asyncio.create_task(catch_interrupt(board.D3))
    await asyncio.gather(interrupt_task)

This program only has one task, so it's not that interesting. But you could use the techniques described on the Communicating Between Tasks page in this guide to alert another task that the interrupt has happened.

The countio is good for catching pin transitions. But if you use it with mechanical switches, it will detect multiple counts due to switch bounce. Another good way is the keypad module, which does debouncing, and can handle multiple pins, switches, or buttons easily.

Handling Interrupts with keypad

The CircuitPython keypad module also provides a way of detecting pin transitions. It does not actually use hardware interrupts: instead it polls the pins every few milliseconds.

An example of using keypad was already presented in this guide on the Communicating Between Tasks page. Here's another example, simplified to show just the transition detection.

import asyncio
import board
import keypad

async def catch_pin_transitions(pin):
    """Print a message when pin goes low and when it goes high."""
    with keypad.Keys((pin,), value_when_pressed=False) as keys:
        while True:
            event =
            if event:
                if event.pressed:
                    print("pin went low")
                elif event.released:
                    print("pin went high")
            await asyncio.sleep(0)

async def main():
    interrupt_task = asyncio.create_task(catch_pin_transitions(board.D3))
    await asyncio.gather(interrupt_task)


  • Monitor pin interrupts with asyncio by making a task that polls for asynchronous pin transitions.
  • Detect asynchronous pin transitions with countio or keypad.Keys.

This guide was first published on Nov 23, 2021. It was last updated on 2023-12-05 11:35:11 -0500.

This page (Handling Interrupts) was last updated on Nov 21, 2021.

Text editor powered by tinymce.