User Guide
Installation
pip install fixfield
# or with uv
uv add fixfield
Requires Python 3.14+.
Core concepts
FixedDecimal
A scalar decimal value locked to a declared precision. Arithmetic always rounds the result to the left operand’s precision.
from fixfield import FixedDecimal
price = FixedDecimal("19.999", places=2) # rounded to 19.00 on construction
str(price) # "20.00"
result = price + FixedDecimal("1.005", places=4)
result.places # 2 — left operand wins
str(result) # "21.00"
Float inputs are converted via str() to avoid binary imprecision:
FixedDecimal(0.1 + 0.2, places=2) # "0.30" ✓
Field
A descriptor that enforces precision on every assignment. Declare it inside a Record.
from fixfield import Field, RoundingStrategy
class Invoice(Record):
price = Field(places=2)
tax_rate = Field(places=4, rounding=RoundingStrategy.ROUND_FLOOR)
total = Field(places=2, default="0.00")
capped = Field(places=2, digits=5) # raises FieldOverflowError above 99999.99
| Parameter | Default | Description |
|---|---|---|
places |
2 |
Decimal places |
rounding |
ROUND_HALF_UP |
Rounding strategy |
default |
None |
Value when not supplied to __init__ |
digits |
None |
Max integer digits (enables overflow protection and to_string) |
signed |
True |
Set False to reject negative values |
Record
A structured collection of Field descriptors. Generates __init__, __repr__, and __eq__ automatically. Fields are processed in declaration order.
from fixfield import Record, Field
class Payment(Record):
amount = Field(places=2, digits=7)
fee = Field(places=2, digits=4)
net = Field(places=2, digits=7)
p = Payment(amount="1000.00", fee="2.50")
p.net = p.amount - p.fee
print(repr(p)) # Payment(amount=1000.00, fee=2.50, net=997.50)
p.to_dict() # {"amount": FixedDecimal(...), ...}
Pass serializable=True to enable to_string / from_string and require digits on all fields:
class Payment(Record, serializable=True):
amount = Field(places=2, digits=7)
fee = Field(places=2, digits=4)
FieldTemplate
A reusable preset for a field. The built-in templates are ready to use:
| Template | places |
digits |
signed |
|---|---|---|---|
CurrencyField() |
2 | 10 | True |
PercentField() |
4 | 3 | True |
QuantityField() |
3 | 7 | False |
RateField() |
6 | 2 | True |
AccountNumberField() |
0 | 12 | False |
All built-in templates are FieldTemplate instances, so you can call them like Field:
from fixfield import CurrencyField, PercentField
class Invoice(Record):
price = CurrencyField()
tax_rate = PercentField()
total = CurrencyField()
Define your own:
from fixfield import FieldTemplate
MoneyField = FieldTemplate(places=2, digits=10, signed=True)
RateField = FieldTemplate(places=6, digits=2, signed=False)
RecordField
Embed a nested Record as a single field. Its fields participate in to_string / from_string as a contiguous block.
from fixfield import Record, Field, RecordField
class Address(Record):
zip_code = Field(places=0, digits=5)
class Customer(Record):
customer_id = Field(places=0, digits=6)
address = RecordField(Address)
c = Customer(customer_id="42", address=Address(zip_code="90210"))
line = c.to_string() # " 42 90210"
parsed = Customer.from_string(line)
str(parsed.address.zip_code) # "90210"
RecordField[T] is generic — your IDE knows c.address is an Address.
ExternalField
A pass-through descriptor for non-decimal values (UUIDs, strings, enums, plain ints, …) inside a Record. Participates in __init__, __repr__, __eq__, to_dict(), to_json(), and from_json(), but not in to_string / from_string.
import uuid
from fixfield import Record, ExternalField, CurrencyField
class Order(Record):
order_id = ExternalField(uuid.UUID, default_factory=uuid.uuid4,
json_encoder=str, json_decoder=uuid.UUID)
reference = ExternalField(str, default="")
total = CurrencyField()
o = Order(reference="ORD-001", total="99.99")
print(type(o.order_id)) # <class 'uuid.UUID'>
print(o.to_json())
# {"order_id": "...", "reference": "ORD-001", "total": "99.99"}
restored = Order.from_json(o.to_json())
assert restored.order_id == o.order_id
| Parameter | Default | Description |
|---|---|---|
field_type |
object |
Expected Python type; checked on every __set__ |
default |
None |
Static default value |
default_factory |
None |
Callable invoked once per instance to produce a default |
json_encoder |
str |
Serialises the value for to_json() |
json_decoder |
field_type |
Deserialises the JSON value in from_json() |
default and default_factory are mutually exclusive.
ExternalField is blocked inside serializable=True records.
RoundingStrategy
from fixfield import RoundingStrategy
RoundingStrategy.ROUND_HALF_UP # 2.5 → 3 (COBOL ROUNDED default)
RoundingStrategy.ROUND_HALF_DOWN # 2.5 → 2
RoundingStrategy.ROUND_HALF_EVEN # 2.5 → 2, 3.5 → 4 (banker's rounding)
RoundingStrategy.ROUND_HALF_ODD # 2.5 → 3, 3.5 → 3
RoundingStrategy.ROUND_UP # always away from zero
RoundingStrategy.ROUND_DOWN # always toward zero (truncate)
RoundingStrategy.ROUND_CEILING # toward +∞
RoundingStrategy.ROUND_FLOOR # toward -∞
Fixed-width serialisation
When every Field has digits set, a Record can be serialised to and from a fixed-width string — useful for mainframe flat files and legacy EDI formats. Use serializable=True to opt in (it checks all fields at class-definition time).
class CustomerRecord(Record, serializable=True):
customer_id = Field(places=0, digits=6) # 7 chars: " 123456"
balance = Field(places=2, digits=8) # 11 chars: " 1234.56"
rec = CustomerRecord(customer_id="123456", balance="1234.56")
line = rec.to_string() # " 123456 1234.56"
parsed = CustomerRecord.from_string(line)
assert parsed == rec
Field width formula: 1 (sign) + digits + (1 + places if places > 0 else 0)
JSON serialisation
import json
from fixfield import Record, CurrencyField
class Invoice(Record):
price = CurrencyField()
total = CurrencyField()
inv = Invoice(price="19.99", total="21.64")
payload = inv.to_json()
# '{"price": "19.99", "total": "21.64"}'
restored = Invoice.from_json(payload)
assert restored == inv
Nested RecordField values are serialised inline. ExternalField values use their configured json_encoder / json_decoder.
Overflow protection
from fixfield import Field, Record, FieldOverflowError
class Account(Record):
balance = Field(places=2, digits=5) # max 99999.99
a = Account()
a.balance = "99999.99" # ✓
a.balance = "100000.00" # raises FieldOverflowError
Why not just use decimal.Decimal?
decimal.Decimal |
fixfield |
|
|---|---|---|
| Precision location | Global context or per .quantize() |
Declared on the field, enforced automatically |
| Per-field rounding | Manual on every result | Declarative |
| Domain modelling | Plain values | Named record schema |
| Non-decimal fields | Anything | ExternalField |
| Legacy serialisation | Manual | to_string / from_string |
Dataclass integration
For users who prefer @dataclass, use FixedDecimal directly:
from dataclasses import dataclass, field
from fixfield import FixedDecimal
@dataclass
class LineItem:
price: FixedDecimal = field(default_factory=lambda: FixedDecimal(0, places=2))
quantity: int = 1
def __post_init__(self):
if not isinstance(self.price, FixedDecimal):
self.price = FixedDecimal(self.price, places=2)
@property
def total(self) -> FixedDecimal:
return self.price * self.quantity
For full per-field precision enforcement without boilerplate, use Record instead.