Skip to main content

TYTX Logo

PyPI JSR Python 3.10+ License Status
Tests Coverage Documentation

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 @genrojs/tytx   # npm
bunx jsr add @genro/tytx    # JSR
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.

Streams of records (JSON Lines)

to_tytx_iter / from_tytx_iter (JavaScript toTytxIter / fromTytxIter) take an iterable of objects and give back an iterator, one record at a time, so a selection is never held in memory as a whole. Encoding turns typed values into "value::CODE" strings with no ::JS marker: each record is plain JSON, one JSONL line. Decoding turns those strings back into typed values and leaves everything else unchanged. A sync source gives an iterator, an async source an async iterator. Splitting the stream into lines belongs to the JSONL reader or writer.

from genro_tytx import to_tytx_iter, from_tytx_iter

for record in to_tytx_iter(rows):                  # rows: dicts with Decimal, date, ...
    out.write(json.dumps(record) + "\n")           # {"price": "12.50::N", "day": "2026-10-22::D"}

for record in from_tytx_iter(json.loads(line) for line in open("orders.jsonl")):
    ...                                            # record["price"] is a Decimal

async for record in from_tytx_iter(async_records): # async source -> async iterator
    ...

Over HTTP the stream is sent as Content-Type: application/jsonl with X-TYTX-Transport: jsonl; getTransport returns 'jsonl' for it. A response that ends early looks like a complete one: reporting errors after the headers, or the expected number of records, is up to the application.

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 calls register_subtype(suffix, cls, name=None) (JavaScript registerSubtype), which adds the class under its own name, or under name, and raises if the name already belongs to another class. genro-bag maps symbolic names to Bag classes this way.

Installation

# Python
pip install genro-tytx

# JavaScript/TypeScript, from npm
npm install @genrojs/tytx

# JavaScript/TypeScript, from JSR
bunx jsr add @genro/tytx
# With npm: npx jsr add @genro/tytx

JavaScript includes Decimal, Big, MessagePack and XML dependencies. No separate codec installation is needed. Python extras remain unchanged.

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.4

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

Source distribution (sdist)

Source distribution for genro-tytx 0.16.4
File Size Uploaded Provenance
genro_tytx-0.16.4.tar.gz 669.9 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for genro-tytx 0.16.4
File Interpreter ABI Platform
genro_tytx-0.16.4-py3-none-any.whl Python 3 none any Details

Total release size: 696.6 kB

Release files / genro_tytx-0.16.4.tar.gz

Download URL genro_tytx-0.16.4.tar.gz
Size 669.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bffaa43c00c4c0e87e5b6fc0098354b6e7f8f081d4ba3960b80b220796e86f07
BLAKE2b-256 checksum
How to use checksums
766782402b2734d0c626fa67a8a2fe37c43eca9f4ced9f9ede4999dca72fb142
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 24, 2026.

Transparency log

Release files / genro_tytx-0.16.4-py3-none-any.whl

Download URL genro_tytx-0.16.4-py3-none-any.whl
Size 26.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9e53eb4b759b94546d7277a35f63189059cbee52ebe9f454342bab1b110ddb7f
BLAKE2b-256 checksum
How to use checksums
ad0146c5d7b3246f68e6233bf180865fc52bba73d72bf3e17878abd918e646c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 24, 2026.

Transparency log

Security for genro-tytx 0.16.4

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for genro_tytx-0.16.4.tar.gz

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 Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.16.4 This release

2 release files Full provenance

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