Skip to main content

Finsy P4Runtime Controller Library

pypi documentation ci codecov codespace

Finsy is a P4Runtime controller library written in Python using asyncio. Finsy includes support for gNMI.

Check out the examples directory for some demonstration programs.

Installation

Finsy requires Python 3.10 or later. To install the latest version, type pip install finsy.

P4Runtime Scripts

With Finsy, you can write a Python script that reads/writes P4Runtime entities for a single switch.

Here is a complete example that retrieves the P4Info from a switch:

import finsy as fy

async def main():
    async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
        # Print out a description of the switch's P4Info, if one is configured.
        print(sw1.p4info)

fy.run(main())

Here is another example that prints out all non-default table entries.

import finsy as fy

async def main():
    async with fy.Switch("sw1", "127.0.0.1:50001") as sw1:
        # Do a wildcard read for table entries.
        async for entry in sw1.read(fy.P4TableEntry()):
            print(entry)

fy.run(main())

P4Runtime Controller

You can also write a P4Runtime controller that manages multiple switches independently. Your controller can react to events from the Switch by changing the contents of P4 tables.

Each switch is managed by an async ready_handler function. Your ready_handler function can read or update various P4Runtime entities in the switch. It can also create tasks to listen for packets or digests.

When you write P4Runtime updates to the switch, you use a unary operator (+, -, ~) to specify the operation: INSERT (+), DELETE (-) or MODIFY (~).

import finsy as fy

async def ready_handler(sw: fy.Switch):
    await sw.delete_all()
    await sw.write(
        [
            # Insert (+) multicast group with ports 1, 2, 3 and CONTROLLER.
            +fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3, 255]),
            # Modify (~) default table entry to flood all unmatched packets.
            ~fy.P4TableEntry(
                "ipv4",
                action=fy.Action("flood"),
                is_default_action=True,
            ),
        ]
    )

    async for packet in sw.read_packets():
        print(f"{sw.name}: {packet}")

Use the SwitchOptions class to specify each switch's settings, including the p4info/p4blob and ready_handler. Use the Controller class to drive multiple switch connections. Each switch will call back into your ready_handler function after the P4Runtime connection is established.

from pathlib import Path

options = fy.SwitchOptions(
    p4info=Path("hello.p4info.txt"),
    p4blob=Path("hello.json"),
    ready_handler=ready_handler,
)

controller = fy.Controller([
    fy.Switch("sw1", "127.0.0.1:50001", options),
    fy.Switch("sw2", "127.0.0.1:50002", options),
    fy.Switch("sw3", "127.0.0.1:50003", options),
])

fy.run(controller.run())

Your ready_handler can spawn concurrent tasks with the Switch.create_task method. Tasks created this way will have their lifetimes managed by the switch object.

If the switch disconnects or its role changes to backup, the task running your ready_handler (and any tasks it spawned) will be cancelled and the ready_handler will begin again.

For more examples, see the examples directory.

Switch Read/Write API

The Switch class provides the API for interacting with P4Runtime switches. You will control a Switch object with a "ready handler" function. The ready handler is an async function that is called when the switch is ready to accept commands.

Your ready handler will typically write some control entities to the switch, then listen for incoming events and react to them with more writes. You may occasionally read entities from the switch.

When your ready handler is invoked, there is already a P4Runtime channel established, with client arbitration completed, and pipeline configured as specified in SwitchOptions.

Here is an example skeleton program. The ready handler is named ready().

async def ready(switch: fy.Switch):
    # Check if switch is the primary. If not, we may want to proceed
    # in read-only mode. In this example, ignore switch if it's a backup.
    if not switch.is_primary:
        return

    # If we're reconnecting to a switch, it will already have runtime state.
    # In this example, we just delete all entities and start over.
    await switch.delete_all()

    # Provision the pipeline with one or more `write` transactions. Each
    # `write` is a single WriteRequest which may contain multiple updates.
    await switch.write(
        # [Next section will cover what goes here.]
    )

    # Listen for events and respond to them. This "infinite" loop will
    # continue until the Switch disconnects, changes primary/backup status,
    # or the controller is stopped.
    async for packet in switch.read_packets():
        await handle_packet(switch, packet)

The Switch class provides a switch.create_task method to start a managed task. Tasks allow you to perform concurrent operations on the same switch. We could have written the last stanza above that reads packets in an infinite loop as a separate task. It's okay for the ready handler function to return early; any tasks it created will still run.

Writes

Use the write() method to write one or more P4Runtime updates and packets.

A P4Runtime update supports one of three operations: INSERT, MODIFY or DELETE. Some entities support all three operations. Other entities only support MODIFY.

Entity Operations Permitted Related Classes
P4TableEntry INSERT, MODIFY, DELETE Match, Action, IndirectAction, P4MeterConfig, P4CounterData, P4MeterCounterData
P4ActionProfileMember INSERT, MODIFY, DELETE
P4ActionProfileGroup INSERT, MODIFY, DELETE P4Member
P4MulticastGroupEntry INSERT, MODIFY, DELETE
P4CloneSessionEntry INSERT, MODIFY, DELETE
P4DigestEntry INSERT, MODIFY, DELETE
P4ExternEntry INSERT, MODIFY, DELETE
P4RegisterEntry MODIFY
P4CounterEntry MODIFY P4CounterData
P4DirectCounterEntry MODIFY P4CounterData
P4MeterEntry MODIFY P4MeterConfig, P4MeterCounterData
P4DirectMeterEntry MODIFY
P4ValueSetEntry MODIFY P4ValueSetMember

Insert/Modify/Delete Updates

To specify the operation, use a unary + (INSERT), ~ (MODIFY), or - (DELETE). If you do not specify the operation, write will raise a ValueError exception.

Here is an example showing how to insert and delete two different entities in the same WriteRequest.

await switch.write([
    +fy.P4TableEntry(          # unary + means INSERT
        "ipv4", 
        match=fy.Match(dest="192.168.1.0/24"),
        action=fy.Action("forward", port=1),
    ),
    -fy.P4TableEntry(          # unary - means DELETE
        "ipv4", 
        match=fy.Match(dest="192.168.2.0/24"),
        action=fy.Action("forward", port=2),
    ),
])

You should not insert, modify or delete the same entry in the same WriteRequest.

If you are performing the same operation on all entities, you can use the Switch insert, delete, or modify methods.

await switch.insert([
    fy.P4MulticastGroupEntry(1, replicas=[1, 2, 3]),
    fy.P4MulticastGroupEntry(2, replicas=[4, 5, 6]),
])

Modify-Only Updates

For entities that only support the modify operation, you do not need to specify the operation. (You can optionally use ~.)

await switch.write([
    fy.P4RegisterEntry("reg1", index=0, data=0),
    fy.P4RegisterEntry("reg1", index=1, data=1),
    fy.P4RegisterEntry("reg1", index=2, data=2),
])

You can also use the modify method:

await switch.modify([
    fy.P4RegisterEntry("reg1", index=0, data=0),
    fy.P4RegisterEntry("reg1", index=1, data=1),
    fy.P4RegisterEntry("reg1", index=2, data=2),
])

If you pass a modify-only entity to the insert or delete methods, the P4Runtime server will return an error.

Sending Packets

Use the write method to send a packet.

await switch.write([fy.P4PacketOut(b"a payload.....", port=3)])

You can include other entities in the same call. Any non-update objects (e.g. P4PacketOut, P4DigestListAck) will be sent before the WriteRequest.

Listening for Packets

To receive packets, use the async iterator Switch.read_packets(). In this example, pkt is a P4PacketIn object.

read_packets can filter for a specific eth_type.

# Read packets filtering only for ARP (eth_type == 0x0806).
async for pkt in switch.read_packets(eth_types={0x0806}):
    # You can access the packet payload `pkt.payload` or any metadata value,
    # e.g. `pkt['ingress_port']`
    print(pkt.payload)
    print(pkt['ingress_port'])

Listening for Digests

To receive digests, use the async iterator Switch.read_digests. You must specify the name of the digest from your P4 program.

async for digest in switch.read_digests("digest_t"):
    # You can access the digest metadata e.g. `digest['ingress_port']`
    # Your code may need to update table entries based on the digest data.
    # To ack the digest, write `digest.ack()`.
    await switch.write([entry, ...])
    await switch.write([digest.ack()])

To acknowledge the digest entry, you can write digest.ack().

Listening for Idle Timeouts

To receive idle timeout notifications, use the async iterator Switch.read_idle_timeouts. You will receive a P4IdleTimeoutNotification which contains multiple table entries -- one for each entry that timed out.

async for timeout in switch.read_idle_timeouts():
    for entry in timeout.table_entry:
        print(timeout.timestamp, entry)

Other Events

A P4 switch may report other events using the EventEmitter API. See the SwitchEvent class for the event types. Each switch has a switch.ee attribute that lets your code register for event callbacks.

Development and Testing

Perform these steps to set up your local environment for Finsy development, or try the codespace. Finsy requires Python 3.10 or later. Install uv if it is not installed.

Set Up Development Environment

Use uv to set up the development environment (venv).

$ git clone https://github.com/byllyfish/finsy.git
$ cd finsy
$ uv venv && uv sync

Run Unit Tests

When you run pytest from the top level of the repository, you will run the unit tests.

$ uv run pytest

Run Integration Tests

When you run pytest from within the examples directory, you will run the integration tests. The integration tests run the example programs against a Mininet network. Docker or podman are required.

$ cd examples
$ uv run pytest

Metadata

Release files for finsy 0.30.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for finsy 0.30.0
File Size Uploaded
finsy-0.30.0.tar.gz 199.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for finsy 0.30.0
File Interpreter ABI Platform
finsy-0.30.0-py3-none-any.whl Python 3 none any Details

Total release size: 446.8 kB

Release files / finsy-0.30.0.tar.gz

Download URL finsy-0.30.0.tar.gz
Size 199.7 kB
Tags Source
SHA-256 checksum
How to use checksums
fd887a10ce7bed8147dec52e5f5bd5513efb485693409422c2a700f96068b310
BLAKE2b-256 checksum
How to use checksums
0eb21ea2f2f42c6b1b7b28ebaf43fb9bdf89814cf83601cf280940bdd0a26174
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 28, 2026.

Transparency log

Release files / finsy-0.30.0-py3-none-any.whl

Download URL finsy-0.30.0-py3-none-any.whl
Size 247.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
29f5194b40ac1c19caacbc4a138b3208b1efa1a6d930c51bccd834d273d22097
BLAKE2b-256 checksum
How to use checksums
c190918ee9164264ca383c2d2869109311c26aa294bfbec3567c287677a7ab4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jun 28, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.30.0 This release

2 release files

0.29.0

2 release files

0.28.0

2 release files

0.26.0

2 release files

0.25.0

2 release files

0.22.0

2 release files

0.21.0

2 release files

0.20.0

2 release files

0.18.0

2 release files

0.16.0

2 release files

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page