Skip to main content

PyBorsh

CI codecov PyPI version Python 3.11+ License: MIT

Pydantic-native Borsh serialization for Python.

PyBorsh lets you define data structures using standard Pydantic models and serialize them to the Borsh binary format—the same format used by Solana, NEAR, and other blockchain ecosystems.

Features

  • 🎯 Pydantic-native — Use standard Pydantic models with full validation, JSON export, and all other Pydantic features
  • 🔒 Type-safe — Explicit integer width annotations (U8, U32, U128, etc.) prevent overflow bugs
  • ⚡ Fast — Direct binary serialization without intermediate representations
  • 🦀 Rust-compatible — 100% compatible with Rust's borsh crate for cross-language interop
  • 📐 Canonical — Bijective object↔bytes mapping per the Borsh spec: maps and sets are encoded sorted by key, NaN is rejected, and from_borsh requires the full input to be consumed. Like Rust borsh, collections of zero-sized element types (e.g. list[EmptyModel]) are rejected on both encode and decode

Installation

pip install pyborsh

Or with uv:

uv add pyborsh

Quick Start

from typing import Annotated
from pydantic import BaseModel
from pyborsh import Borsh, U8, U32, U128, Bytes

class Player(Borsh, BaseModel):
    name: str
    health: Annotated[int, U8]       # u8: 0-255
    score: Annotated[int, U32]       # u32: 0-4,294,967,295
    balance: Annotated[int, U128]    # u128: for large numbers
    guild: str | None                # Option<String>
    pubkey: Annotated[bytes, Bytes(32)]  # [u8; 32] fixed-size

# Create a player (standard Pydantic)
player = Player(
    name="Alice",
    health=100,
    score=50_000,
    balance=1_000_000_000_000_000_000,
    guild="Warriors",
    pubkey=bytes(32),
)

# All Pydantic features work
player.model_dump()
player.model_dump_json()
Player.model_validate({"name": "Bob", ...})

# Borsh serialization
data: bytes = player.to_borsh()
restored = Player.from_borsh(data)
assert player == restored

Type Mapping

PyBorsh maps Python types to Borsh types:

Python Type Borsh Type Notes
Annotated[int, U8] u8 Unsigned 8-bit
Annotated[int, U16] u16 Unsigned 16-bit
Annotated[int, U32] u32 Unsigned 32-bit
Annotated[int, U64] u64 Unsigned 64-bit
Annotated[int, U128] u128 Unsigned 128-bit
Annotated[int, I8] i8 Signed 8-bit
Annotated[int, I16] i16 Signed 16-bit
Annotated[int, I32] i32 Signed 32-bit
Annotated[int, I64] i64 Signed 64-bit
Annotated[int, I128] i128 Signed 128-bit
Annotated[float, F32] f32 32-bit float
float f64 64-bit float (default)
bool bool Boolean
str String UTF-8 string
bytes Vec<u8> Dynamic bytes
Annotated[bytes, Bytes(N)] [u8; N] Fixed-size bytes
list[T] Vec<T> Dynamic array
Annotated[list[T], Array(T, N)] [T; N] Fixed-size array
set[T] HashSet<T> Encoded sorted by element
dict[K, V] HashMap<K, V> Encoded sorted by key
tuple[A, B, C] (A, B, C) Fixed tuple
T | None Option<T> Optional value
NestedModel struct Nested struct
IntEnum u8 Simple enum
BorshEnum variants enum Rust-style tagged union

Examples

Collections

from typing import Annotated
from pydantic import BaseModel
from pyborsh import Borsh, U8, U16, U32, Array

class GameState(Borsh, BaseModel):
    # Vec<u16> - dynamic list
    scores: list[Annotated[int, U16]]

    # [u8; 4] - fixed array
    color: Annotated[list[int], Array(U8, 4)]

    # HashMap<String, u32>
    inventory: dict[str, Annotated[int, U32]]

    # HashSet<String>
    tags: set[str]

    # (u8, String, u32) - heterogeneous tuple
    metadata: tuple[Annotated[int, U8], str, Annotated[int, U32]]

Nested Structs

class Stats(Borsh, BaseModel):
    strength: Annotated[int, U8]
    agility: Annotated[int, U8]
    intelligence: Annotated[int, U8]

class Character(Borsh, BaseModel):
    name: str
    stats: Stats                    # Nested struct
    ally: Stats | None              # Optional nested struct
    party: list[Stats]              # Vec of structs

Rust-Style Enums (Tagged Unions)

For Rust enums with associated data, use BorshEnum:

from typing import Literal
from pydantic import BaseModel
from pyborsh import Borsh, BorshEnum, U32, U64

class Message(BorshEnum):
    """Equivalent to Rust:
    enum Message {
        Quit,
        Move { x: u32, y: u32 },
        Write(String),
        ChangeColor(u8, u8, u8),
    }
    """
    class Quit(Borsh, BaseModel):
        variant: Literal["Quit"] = "Quit"

    class Move(Borsh, BaseModel):
        variant: Literal["Move"] = "Move"
        x: Annotated[int, U32]
        y: Annotated[int, U32]

    class Write(Borsh, BaseModel):
        variant: Literal["Write"] = "Write"
        message: str

class Packet(Borsh, BaseModel):
    id: Annotated[int, U64]
    payload: Message.Quit | Message.Move | Message.Write

# Usage
packet = Packet(
    id=1,
    payload=Message.Move(x=10, y=20)
)
data = packet.to_borsh()

Simple Enums

For simple enums without data, use IntEnum:

from enum import IntEnum
from typing import Annotated
from pydantic import BaseModel
from pyborsh import Borsh, U32

class Status(IntEnum):
    PENDING = 0
    ACTIVE = 1
    COMPLETED = 2

class Task(Borsh, BaseModel):
    id: Annotated[int, U32]
    status: Status  # Serialized as u8

Rust Interoperability

PyBorsh produces byte-for-byte identical output to Rust's borsh crate:

Rust:

use borsh::{BorshSerialize, BorshDeserialize};

#[derive(BorshSerialize, BorshDeserialize)]
struct Player {
    name: String,
    health: u8,
    balance: u128,
}

let player = Player {
    name: "Alice".to_string(),
    health: 100,
    balance: 1_000_000_000,
};
let bytes = borsh::to_vec(&player).unwrap();

Python:

class Player(Borsh, BaseModel):
    name: str
    health: Annotated[int, U8]
    balance: Annotated[int, U128]

player = Player(name="Alice", health=100, balance=1_000_000_000)
data = player.to_borsh()
# `data` is identical to Rust's `bytes`

Error Handling

PyBorsh provides descriptive errors:

from pyborsh import BorshSchemaError, BorshSerializationError, BorshDeserializationError

# Schema errors (at definition time)
class Bad(Borsh, BaseModel):
    value: int  # Error: int requires explicit width (use U8, U32, etc.)

# Serialization errors
player = Player(health=256, ...)  # Error: 256 out of range for u8
Temperature(celsius=float("nan")).to_borsh()  # Error: NaN is not a valid Borsh value

# Deserialization errors
Player.from_borsh(b"corrupted")  # Error: Unexpected end of data
Player.from_borsh(valid_bytes + b"\x00")  # Error: Not all bytes read: 1 trailing bytes

Deserialization failures always raise BorshDeserializationError (invalid enum discriminants, malformed UTF-8, truncated input, trailing bytes), and serialization failures raise BorshSerializationError—no bare ValueError or struct.error leaks through.

Development

# Clone and install
git clone https://github.com/r-near/pyborsh.git
cd pyborsh
uv sync --all-extras

# Run tests (includes a Hypothesis property-based suite covering round-trip,
# determinism, and malformed-input behavior; HYPOTHESIS_PROFILE=ci for more examples)
uv run pytest

# Run linting
uv run ruff check src/ tests/
uv run mypy src/

# Install pre-commit hooks
uv run pre-commit install

License

MIT

Metadata

Release files for pyborsh 1.1.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 pyborsh 1.1.0
File Size Uploaded
pyborsh-1.1.0.tar.gz 89.8 kB Details

Built distribution (wheel)

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

Total release size: 107.8 kB

Release files / pyborsh-1.1.0.tar.gz

Download URL pyborsh-1.1.0.tar.gz
Size 89.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e9788d728cab2b7302c4ddef19f0e135e71f0a9569ea17d5554784a15f497c49
BLAKE2b-256 checksum
How to use checksums
8f5581f1970567f0a0884a8969d8babf88cb0f378252b53de6550342e1adb25a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Jul 21, 2026.

Transparency log

Release files / pyborsh-1.1.0-py3-none-any.whl

Download URL pyborsh-1.1.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2adc4841352167e04bab01b1c9fac631db998fbdd66e8531486f32b8c5333b57
BLAKE2b-256 checksum
How to use checksums
60191e956486b3f0eb19e6b5516dab4a4e122144aa79f5e25a289fe78773298c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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 Jul 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.2

2 release files

1.0.1

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