genro-tytx
A lightweight multi-transport typed data interchange system.
TYTX eliminates manual type conversions between Python and JavaScript, and makes switching to MessagePack for better performance as simple as changing a parameter.
You send a Decimal from Python, JavaScript receives a string. You convert it back. Every. Single. Time. TYTX fixes this—types flow automatically between Python and JavaScript, over JSON, XML, or MessagePack.
The Pain You Know
# Your Python API
return {"price": Decimal("99.99"), "due_date": date(2025, 1, 15)}
// Your JavaScript client
const data = await response.json();
// data.price is "99.99" (string) - need to convert
// data.due_date is "2025-01-15" (string) - need to convert
const price = new Decimal(data.price); // Manual conversion
const dueDate = new Date(data.due_date); // Manual conversion
This leads to:
- Conversion code scattered everywhere
- Bugs when someone forgets to convert
- Financial calculations with floating-point errors
- Different date formats causing off-by-one-day bugs
The TYTX Solution
# Server - just return native types
return {"price": Decimal("99.99"), "due_date": date(2025, 1, 15)}
// Client - types arrive ready to use
const data = await fetchTytx('/api/order');
data.price // → Decimal (not string)
data.due_date // → Date (not string)
Zero conversion code. Types just work.
30-Second Demo
Python:
pip install genro-tytx
from decimal import Decimal
from datetime import date
from genro_tytx import to_tytx, from_tytx
# Encode
data = {"price": Decimal("99.99"), "date": date(2025, 1, 15)}
encoded = to_tytx(data)
# '{"price": "99.99::N", "date": "2025-01-15::D"}::JS'
# Decode
decoded = from_tytx(encoded)
# {"price": Decimal("99.99"), "date": date(2025, 1, 15)}
JavaScript:
npm install genro-tytx big.js
import { fetchTytx } from 'genro-tytx';
import Big from 'big.js';
const result = await fetchTytx('/api/invoice', {
body: { price: new Big('99.99'), date: new Date() }
});
// result.total → Big (ready to use)
Untyped JSON codec
Besides the typed to_tytx / from_tytx API, TYTX exposes a plain JSON codec for the untyped path — no type suffixes, just fast JSON (orjson when available, stdlib otherwise):
from genro_tytx import json_dumps, json_loads
json_dumps({"a": 1}) # -> b'{"a":1}' (UTF-8 bytes)
json_loads('{"a": 1}') # accepts str or bytes -> {"a": 1}
json_dumps returns UTF-8 bytes (ready for ASGI/WebSocket send), json_loads accepts str or bytes. Use these when you want plain JSON; use to_tytx / from_tytx when you want typed values to flow.
Custom types
register_type lets an external package teach TYTX a new type — a serialize hook (object → string) and a deserialize hook (string → object) under a custom suffix. The package registers at its own import time, so TYTX gains no dependency on it:
from genro_tytx import register_type, to_tytx, from_tytx
class Point:
def __init__(self, x, y): self.x, self.y = x, y
register_type(Point, "PT", lambda p: f"{p.x},{p.y}",
lambda s: Point(*map(int, s.split(","))))
to_tytx([1, Point(5, 6), "k"]) # '[1,"5,6::PT","k"]::JS'
from_tytx('5,6::PT') # Point(5, 6)
The custom type flows like any built-in scalar, including nested inside dicts and lists. The exact type is matched first; an unregistered subclass of a registered type travels under that type's code, written by its own serializer (genro-builders' SourceBag travels as genro-bag's X). Built-in types keep the exact-type rule. A class cannot be registered under a code another class owns. A code is one or more uppercase ASCII letters, any length; registration refuses anything else. Re-registering the same class replaces its hooks; reusing a suffix owned by a different type raises an error. An unknown code on the receiving side is not an error: the string comes back untouched. The same semantics apply to the JavaScript client (registerType / registerClass). The rules and the codes reserved by consumers are in spec/TYTX-SPEC.md §2.5.
When the type owns its serialization, register_class reads the hooks from the class itself — usable as a decorator. It needs __tytx_suffix__, an instance to_tytx() and a from_tytx classmethod (from_tytx must be a classmethod: decoding starts from the suffix and rebuilds the instance from scratch):
from genro_tytx import register_class
@register_class
class Point:
__tytx_suffix__ = "PT"
def __init__(self, x, y): self.x, self.y = x, y
def to_tytx(self): return f"{self.x},{self.y}"
@classmethod
def from_tytx(cls, s): return cls(*map(int, s.split(",")))
The concrete class of a subclass is the type's own business. TYTX keeps one subtype dictionary per code and never reads it: set_subtype_dict(suffix, dict) replaces it, get_subtype_dict(suffix) returns it ({} if none was set). JavaScript: setSubtypeDict / getSubtypeDict. The type's to_tytx/from_tytx use it; whoever adds a subclass reads the dictionary, adds its entries and sets it again. genro-bag maps symbolic names to Bag classes this way.
Installation
# Python
pip install genro-tytx
# JavaScript/TypeScript
npm install genro-tytx
# Recommended: decimal library for JS
npm install big.js # lightweight, good for most cases
# or: npm install decimal.js # more features
Real-World Example: Order Processing
A typical business scenario: process an order with 8 typed fields, return 6 typed results.
1. The Data
JavaScript Client has order data with proper types:
import Big from 'big.js';
const orderData = {
unit_price: new Big('149.99'), // Decimal
quantity: 3,
discount: new Big('10.00'), // Decimal
order_date: new Date(2025, 0, 15), // Date
delivery_date: new Date(2025, 0, 20),
express: true,
customer_id: 12345,
notes: 'Handle with care'
};
// Expects response: { subtotal, tax, shipping, total, ship_date, arrival_date }
Python Server has business logic expecting proper types:
async def process_order(unit_price, quantity, discount, order_date,
delivery_date, express, customer_id, notes):
"""Business logic - expects Decimal and date types."""
subtotal = unit_price * quantity
tax = subtotal * Decimal('0.22')
shipping = Decimal('15.00') if express else Decimal('5.00')
total = subtotal - discount + tax + shipping
ship_date = order_date + timedelta(days=1 if express else 3)
return {
'subtotal': subtotal, 'tax': tax, 'shipping': shipping,
'total': total, 'ship_date': ship_date, 'arrival_date': delivery_date
}
2. ❌ WITHOUT TYTX: 20 Manual Conversions
JavaScript - must convert to/from strings:
// Send: convert types → strings
const response = await fetch('/api/process_order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
unit_price: orderData.unit_price.toString(), // Decimal → string
discount: orderData.discount.toString(), // Decimal → string
order_date: orderData.order_date.toISOString().slice(0, 10), // Date → string
delivery_date: orderData.delivery_date.toISOString().slice(0, 10),
quantity: orderData.quantity, express: orderData.express,
customer_id: orderData.customer_id, notes: orderData.notes
})
});
// Receive: convert strings → types
const json = await response.json();
const result = {
subtotal: new Big(json.subtotal), tax: new Big(json.tax),
shipping: new Big(json.shipping), total: new Big(json.total),
ship_date: new Date(json.ship_date), arrival_date: new Date(json.arrival_date)
};
Python - must convert to/from strings:
@app.post("/api/process_order")
async def handle_order(request: Request):
json_data = await request.json()
# Receive: convert strings → types
unit_price = Decimal(json_data['unit_price'])
discount = Decimal(json_data['discount'])
order_date = date.fromisoformat(json_data['order_date'])
delivery_date = date.fromisoformat(json_data['delivery_date'])
result = await process_order(unit_price, json_data['quantity'], discount,
order_date, delivery_date, json_data['express'],
json_data['customer_id'], json_data['notes'])
# Send: convert types → strings
return JSONResponse({
'subtotal': str(result['subtotal']), 'tax': str(result['tax']),
'shipping': str(result['shipping']), 'total': str(result['total']),
'ship_date': result['ship_date'].isoformat(),
'arrival_date': result['arrival_date'].isoformat()
})
Total: 20 manual conversions (4 JS→string + 4 string→Python + 6 Python→string + 6 string→JS).
3. ✅ WITH TYTX: Zero Conversions
import { fetchTytx } from 'genro-tytx';
const result = await fetchTytx('/api/process_order', { body: orderData });
console.log(result.total.toFixed(2)); // Big, ready to use
Total: 0 conversions. Types flow naturally.
Bonus: Switch to MessagePack in One Line
Need binary format for better performance? Just add transport: 'msgpack':
// JSON (default)
const result = await fetchTytx('/api/process_order', { body: orderData });
// MessagePack - same API, binary format
const result = await fetchTytx('/api/process_order', { body: orderData, transport: 'msgpack' });
Supported Types
| Python | JavaScript | Wire Format |
|---|---|---|
Decimal |
Decimal (big.js) |
"99.99::N" |
date |
Date (midnight UTC) |
"2025-01-15::D" |
datetime |
Date |
"2025-01-15T10:30:00.000Z::DHZ" |
time |
Date (epoch date) |
"10:30:00.000::H" |
bytes |
Uint8Array |
"AAEC::RAW" (base64; native bin on MessagePack) |
Native JSON types (string, number, boolean, null) pass through unchanged.
When to Use TYTX
Good fit:
- Web apps with forms containing dates/decimals
- Financial applications requiring decimal precision
- APIs that send/receive typed data frequently
- Excel-like grids with mixed types
Not needed:
- APIs that only use strings and integers
- Simple CRUD with no special types
- Already using GraphQL/Protobuf with full type support
Documentation
| I want to... | Go to... |
|---|---|
| Try it in 5 minutes | Quick Start |
| Use it over HTTP | HTTP Integration |
| Understand the wire format | How It Works |
| See API reference | API Reference |
| Compare with alternatives | Alternatives |
License
Apache License 2.0 - Copyright 2025 Softwell S.r.l.
Metadata
Release files for genro-tytx 0.16.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| genro_tytx-0.16.0.tar.gz | 518.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| genro_tytx-0.16.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 543.6 kB
Release files / genro_tytx-0.16.0.tar.gz
| Download URL | genro_tytx-0.16.0.tar.gz |
|---|---|
| Size | 518.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bfeea276d073b18609ae290b2c541084690e05d3242876b319420b68ae30e3a2
|
|
BLAKE2b-256 checksum How to use checksums |
d046f6ee77959a989a87f645b3c252812455b8bf30828957fad19a200a0cb869
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Sep 26, 2026.
Transparency logRelease files / genro_tytx-0.16.0-py3-none-any.whl
| Download URL | genro_tytx-0.16.0-py3-none-any.whl |
|---|---|
| Size | 24.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8f97abc7ede4d82db0ea63d25a9370b87ec6f5b105a35e3f342d949876221d23
|
|
BLAKE2b-256 checksum How to use checksums |
f6c4df463f82ea22769b588a8a53fedb484fafa6528907316bcd6a1a53d820c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Sep 26, 2026.
Transparency log