Pydantic-native Borsh serialization for Python
Project description
PyBorsh
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
borshcrate for cross-language interop
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> |
Hash set |
dict[K, V] |
HashMap<K, V> |
Hash map |
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
# Deserialization errors
Player.from_borsh(b"corrupted") # Error: Unexpected end of data
Development
# Clone and install
git clone https://github.com/r-near/pyborsh.git
cd pyborsh
uv sync --all-extras
# Run tests
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyborsh-1.0.1.tar.gz.
File metadata
- Download URL: pyborsh-1.0.1.tar.gz
- Upload date:
- Size: 69.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de6ab15953c53a0092f5b9152f7f2cc3127743ffba5adb5c1600668a7e0d2d22
|
|
| MD5 |
7928c2cfbda62f2df468388f033aeed8
|
|
| BLAKE2b-256 |
b1aa7f45c63f59bfdcfca0a6852745a14f264b5d9a054515a5fc6c5e5a21c87e
|
Provenance
The following attestation bundles were made for pyborsh-1.0.1.tar.gz:
Publisher:
release.yml on r-near/pyborsh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyborsh-1.0.1.tar.gz -
Subject digest:
de6ab15953c53a0092f5b9152f7f2cc3127743ffba5adb5c1600668a7e0d2d22 - Sigstore transparency entry: 849832784
- Sigstore integration time:
-
Permalink:
r-near/pyborsh@3999459fd16dce7aed7f0757ad06f581d786dad5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/r-near
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3999459fd16dce7aed7f0757ad06f581d786dad5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyborsh-1.0.1-py3-none-any.whl.
File metadata
- Download URL: pyborsh-1.0.1-py3-none-any.whl
- Upload date:
- Size: 14.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4f1c3e17ea35b2dcab450087c78156a16e38410695b45cb9db741cd0d8edffe
|
|
| MD5 |
503d4eb3b0df1bfd11be9b6c0ee1ce97
|
|
| BLAKE2b-256 |
b5ab607777fb88b6522674c694c4dd5f6f7df8f5d029d69deb58732297d361d1
|
Provenance
The following attestation bundles were made for pyborsh-1.0.1-py3-none-any.whl:
Publisher:
release.yml on r-near/pyborsh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyborsh-1.0.1-py3-none-any.whl -
Subject digest:
b4f1c3e17ea35b2dcab450087c78156a16e38410695b45cb9db741cd0d8edffe - Sigstore transparency entry: 849832789
- Sigstore integration time:
-
Permalink:
r-near/pyborsh@3999459fd16dce7aed7f0757ad06f581d786dad5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/r-near
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3999459fd16dce7aed7f0757ad06f581d786dad5 -
Trigger Event:
push
-
Statement type: