This release focuses on improving user quality-of-life, improving documentation, and setting cocotb up for even more ambitious feature work. It is backwards compatible with the 2.0 release.

Check out the list below for the changes we are most proud of and think are the most impactful. If you want the full list, check out the release notes.

New Concurrency Primitives

gather and select

cocotb.triggers.First and cocotb.triggers.Combine support awaiting multiple Triggers at the same time; First waiting for any to finish, and Combine waiting for all. However, they have some serious limitations, the most annoying being that they don’t support being passed Python native coroutines directly.

To address the issues with these functions, new concurrency primitives were added. select and gather were introduced to replace First and Combine, respectively. These new functions support being passed coroutines, or any awaitable object directly, and return more information to the user.

For a full tutorial on these functions and the rest of cocotb’s concurrency system, check out the new docs!

async def drive(intf, transaction_queue):
    while True:
        await RisingEdge(intf.clk)
        intf.valid.value = 0

        # idx is the index into the argument list
        # res is the result of the argument that finished
        idx, res = await select(
            transaction_queue.get(),  # coroutine!!!
            RisingEdge(intf.rst),     # Trigger
        )

        if idx == 0:
            intf.data.value = res.data
            intf.valid.value = 1
        else:
            # got reset, hold valid low until reset is done
            intf.valid.value = 0
            await FallingEdge(intf.rst)

TaskManager

trio introduced to the Python world structured concurrency with Nurserys. The Python standard library asyncio followed with TaskGroup. Even SystemVerilog has fork/join. Now cocotb has TaskManager to fill the same role. If gather or select are not expressive enough for your needs, TaskManager will likely work for you.

async with TaskManager() as tm:

    @tm.fork
    async def drive_intf_a(intf):
        while True:
            ...  # drive interface A

    @tm.fork
    async def drive_intf_b(intf):
        while True:
            ...  # drive interface B

# When the TaskManager block exits it implicitly waits for all Tasks to finish before continuing.
print("Done!")

cocotb Previews

Som improvements to cocotb can only be made by breaking something. Breaking changes are always considered very carefully, and when they occur cocotb gains a new major version. We, the maintainers, do try to prevent major versions from happening too often to make life a bit easier on our users. But the flip side of that coin is that multiple breaking changes can amass over time making it too painful for users to move to the new major version.

To solve this issue, cocotb has gained a new system: “Previews.” Enabling Previews allows cocotb maintainers and developers to make breaking changes without having to wait several years before getting the opportunity. And it allows users to opt-in to the new breaking behavior so they get feedback sooner about whether their codebase is ready for the next major version.

Previews are enabled by setting the COCOTB_PREVIEW environment variable. You can set it to a comma separated list of features, allowing you to adapt to each change, one at a time; or you can simply set it to 1 to enable all changes.

How you use this feature is up to you. Will you leave it off, checking it occasionally, allowing your codebase to take advantage of the backwards compatability guarantee of the cocotb 2.X major release cycle? Or will you leave it set to 1, taking advantage of the newest features and changes on every minor release, thus spreading out the major version upgrade work?

Stackable Test Decorators

The @cocotb.test decorator has a number of arguments for specifying conditions on when to skip tests, mark them as expected to fail or raise an exception, and more. But this makes more complex uses difficult as all “skip” conditions have to be joined into a single ugly expression.

To fix this, we added new “stackable” test decorators: @cocotb.skipif and @cocotb.xfail, and also made @cocotb.parametrize stackable as well. Each cocotb test can take multiple decorators and every condition they specify is joined together before tests are discovered.

@cocotb.skipif(cocotb.top.ENABLE_ETH_B.value == 0, reason="This tests ethernet B, so skip if the device is not configured with it.")
@cocotb.xfail(today() == "Monday", reason="Mondays are the worst.")
@cocotb.parametrize(
    packet_size=[1, 10, 200, 2000]
)
@cocotb.parametrize(
    traffic_generator=["realistic", "stress"]
)
async def test_switch(dut, packet_size: int, traffic_generator: str) -> None:
    ...

Test Forcing Functions

If you’ve ever written a test where the decision the test has passed happens deep in some random Task, or if you’ve been unable to express the logic to skip a test in the test decorator, cocotb now has a set of test end forcing functions to help you.

Users can now call pytest.skip and pytest.xfail in tests to immediately end the test with a forced SKIP or XFAIL result.

@cocotb.parametrize(
    valid_fraction=[1.0, 0.6]
    ready_fraction=[1.0, 0.6],
)
async def test_control_flow(dut, valid_fraction: float, ready_fraction: float) -> None:

    if dut.BUFFERED_OUTPUT.value and ready_fraction != 1.0:
        pytest.skip("If the device is configured with the buffered output, we will never see ready go low.")

    ...

Similarly, the cocotb.end_test function will end the test immediately as if the main test function had ended.

@cocotb.test
async def test_watchdog(dut):

    async def watch_watchdog():
        await RisingEdge(dut.watchdog)
        # End the test from this Task instead of the main test coroutine.
        cocotb.end_test()

    # Run the watcher in the background.
    cocotb.start_soon(watch_watchdog())

    ... # Stimulate a hang.

Scheduler Rewrite

One of the major efforts of the release cycle should be invisible to the user. The coroutine scheduler that cocotb uses to run Tasks concurrently and react to Triggers firing has been rewritten. This rewrite was designed to improve performance, improve readbility and maintainability, and set cocotb up for scheduler-related enhancements in the coming versions.

Platform Support Changes

Technology progresses and we eventually must leave behind old platforms that have become difficult to maintain. 32-bit builds for Linux and Windows are no longer available. Support for Python 3.6, 3.7, and 3.8 have been removed. MacOS 13 support has also been removed.

On the other hand, cocotb gained support for Python 3.14, Ubuntu 26.04 and MacOS 26. Leaving old Python versions behind allows cocotb to use new Python features to ensure correctness and maintainability.