Fixed Point Python Libraries
Introduction
I’ve been exploring ways to simulate fixed-point arithmetic in Python for algorithm development and testing. Fixed-point arithmetic is especially relevant for embedded systems, DSPs, and hardware modeling where floating-point may not be available or efficient. Since there are several libraries, all a bit different I test out as many as I could find.
The test is:
- For each library show simple and compound calculations in fixed point.
- Show the construction of a signed 32-bit value with 16 bit fractional precision.
- Show most positive and most negative value for each (test for resizing etc.)
The code and results are included at the end of the post.
Since I’m interested in fixed-point types that do not change their bit widths during operations, any library that auto-resizes operands or results during computation is not suitable for my use case. Where possible, I’ve disabled automatic resizing.
Libraries Tested
Conclusion
- spfpm behaves exactly like a standard fixed point in a compiled language with exceptions on overflow.
- numfi also behaves like a standard fixed point with overflows at the limits, no exceptions are thrown.
- fpbinary, fxpmath, fixedpoint, & fixedpointtest have automatic dynamic resizing which is an auto fail for this application (still a good libraries just not the right choice for this application.)
For simulations where silent overflow is unacceptable, spfpm is the most robust option thanks to its strict behavior and exception handling. numfi is also solid if wraparound is acceptable and exceptions aren’t required.
Library | Auto Resize | Overflow Handling | Exception on Overflow | Pythonic Syntax | Notes |
---|---|---|---|---|---|
spfpm | ❌ No | Saturates at limits | ✅ Yes | ✅ Yes | Most like hardware behavior, strict typing |
numfi | ❌ No | Wraparound | ❌ No | ✅ Yes | |
fpbinary | ✅ Yes | Grows with operations | ❌ No | ✅ Yes | Type expands with each operation |
fxpmath | ✅ Yes | Grows with operations | ❌ No | ✅ Yes | Good for DSP work, limited abs support |
fixedpoint | ✅ Yes | Auto adjusts size | ❌ No | ✅ Yes | |
fixedpointtest | ✅ Yes | Auto adjusts size | ❌ No | ✅ Yes | More of a testing/simulation tool |
Legend
- Auto Resize: Whether the library automatically increases the bit width during operations.
- Overflow Handling: How the library behaves when value exceeds representable range.
- Exception on Overflow: Whether it raises an error on overflow (preferred for strict simulation).
- Pythonic Syntax: Whether it integrates cleanly with Python idioms (e.g., operator overloading,
abs
, etc.)
Verdict
For strict hardware-style fixed-point simulation:
- Best choice:
spfpm
- Alternative:
numfi
(with wrap-around instead of exception)
Tests & Results
bits = 32
int_bits = 16
frac_bits = bits-int_bits
def testbench(value, type_to_str_f=lambda x : str(type(x))):
def display_result(result):
try:
normal_repr = float(result)
except Exception as e:
normal_repr = str(result)
display(Markdown(f"+ Result: {repr(result)}\n+ float(result): {normal_repr}\n+ type(result): {type_to_str_f(result)}"))
one = (value + 1)-value
display(Markdown("### a"))
display_result(value)
# multiplication
result = value * value
display(Markdown("### a * a"))
display_result(result)
# division
display(Markdown("### a / a"))
try:
result = value / value
display_result(result)
except Exception as e:
display(Markdown(f"+ Result: {e}\n+ type(result): {type(e)}"))
# addition
result = value + value
display(Markdown("### a + a"))
display_result(result)
# subtraction
result = value - value
display(Markdown("### a - a"))
display_result(result)
# power
display(Markdown("### a**a"))
try:
result = value ** value
display_result(result)
except Exception as e:
display(Markdown(f"+ Result: {e}\n+ type(result): {type(e)}"))
# absolute value
display(Markdown("### |a|"))
try:
result = abs(value)
display_result(result)
except Exception as e:
display(Markdown(f"+ Result: {e}\n+ type(result): {type(e)}"))
# most positive number
display(Markdown("### Most Positive Value"))
i = 1
try:
most_positive = one*((1<<(int_bits-1))-i)
display_result(most_positive)
except Exception as e:
display(e, type(e))
# positive overflow
display(Markdown("### Most Positive Value + 1"))
try:
result = most_positive + 1
display_result(result)
except Exception as e:
display(Markdown(f"+ Result: {e}\n+ type(result): {type(e)}"))
# most negative
display(Markdown("### Most Negative Value"))
try:
most_negative = -one*((1<<(int_bits-1))-1) -1
display_result(most_negative)
except Exception as e:
display(e, type(e))
# negative overflow
display(Markdown("### Most Negative Value - 1"))
try:
result = most_negative - 1
display_result(result)
except Exception as e:
display(Markdown(f"+ Result: {e}\n+ type(result): {type(e)}"))
spfpm
- url: https://github.com/rwpenney/spfpm
- Project name: spfpm
- Package name: FixedPoint
- Notes: Has no resizing option
from FixedPoint import FXfamily, FXnum
# data type is called "family"
family = FXfamily(n_bits=frac_bits, n_intbits=int_bits)
# construct
value = FXnum(-3, family=family)
testbench(value, type_to_str_f=lambda x: f"{value.family}")
a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=-196608)
- float(result): -3.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
a * a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=589824)
- float(result): 9.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
a / a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=65535)
- float(result): 0.9999847412109375
- type(result): FXfamily(n_bits=16, n_intbits=16)
a + a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=-393216)
- float(result): -6.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
a - a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=0)
- float(result): 0.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
a**a
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=-2428)
- float(result): -0.03704833984375
- type(result): FXfamily(n_bits=16, n_intbits=16)
|a|
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=196608)
- float(result): 3.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
Most Positive Value
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=2147418112)
- float(result): 32767.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
Most Positive Value + 1
- Result:
- type(result): <class ‘FixedPoint.FXoverflowError’>
Most Negative Value
- Result: FXnum(family=FXfamily(n_bits=16, n_intbits=16), scaled_value=-2147483648)
- float(result): -32768.0
- type(result): FXfamily(n_bits=16, n_intbits=16)
Most Negative Value - 1
- Result:
- type(result): <class ‘FixedPoint.FXoverflowError’>
fpbinary
- url: https://github.com/smlgit/fpbinary
- Project name: fpbinary
- Package name: fpbinary
- Notes: Auto resizes data types
from fpbinary import FpBinary
# construct
value = FpBinary(int_bits=int_bits, frac_bits=frac_bits, signed=True, value=-3)
testbench(value, type_to_str_f=lambda x: f"{x.format}")
a
- Result: -3.0
- float(result): -3.0
- type(result): (16, 16)
a * a
- Result: 9.0
- float(result): 9.0
- type(result): (32, 32)
a / a
- Result: 1.0
- float(result): 1.0
- type(result): (33, 32)
a + a
- Result: -6.0
- float(result): -6.0
- type(result): (17, 16)
a - a
- Result: 0.0
- float(result): 0.0
- type(result): (17, 16)
a**a
- Result: unsupported operand type(s) for ** or pow(): ‘fpbinary.FpBinary’ and ‘fpbinary.FpBinary’
- type(result): <class ‘TypeError’>
|a|
- Result: 3.0
- float(result): 3.0
- type(result): (17, 16)
Most Positive Value
- Result: 32767.0
- float(result): 32767.0
- type(result): (34, 16)
Most Positive Value + 1
- Result: 32768.0
- float(result): 32768.0
- type(result): (35, 16)
Most Negative Value
- Result: -32768.0
- float(result): -32768.0
- type(result): (36, 16)
Most Negative Value - 1
- Result: -32769.0
- float(result): -32769.0
- type(result): (37, 16)
fxpmath
- url: https://github.com/francof2a/fxpmath
- Project name: fxpmath
- Package name: fxpmath
- Notes: Auto resizes data type
from fxpmath import Fxp
# construct
value = Fxp(val=-3, dtype=f'fxp-s{bits}/{frac_bits}', rounding="fix", Shifting="trunc")
testbench(value, type_to_str_f=lambda x: f"{x.dtype}")
a
- Result: fxp-s32/16(-3.0)
- float(result): -3.0
- type(result): fxp-s32/16
a * a
- Result: fxp-s64/32(9.0)
- float(result): 9.0
- type(result): fxp-s64/32
a / a
- Result: fxp-s64/31(1.0)
- float(result): 1.0
- type(result): fxp-s64/31
a + a
- Result: fxp-s33/16(-6.0)
- float(result): -6.0
- type(result): fxp-s33/16
a - a
- Result: fxp-s33/16(0.0)
- float(result): 0.0
- type(result): fxp-s33/16
a**a
- Result: fxp-s53/52(-0.03703703703703698)
- float(result): -0.03703703703703698
- type(result): fxp-s53/52
|a|
- Result: bad operand type for abs(): ‘Fxp’
- type(result): <class ‘TypeError’>
Most Positive Value
- Result: fxp-s33/16(32767.0)
- float(result): 32767.0
- type(result): fxp-s33/16
Most Positive Value + 1
- Result: fxp-s33/16(32768.0)
- float(result): 32768.0
- type(result): fxp-s33/16
Most Negative Value
- Result: fxp-s33/16(-32768.0)
- float(result): -32768.0
- type(result): fxp-s33/16
Most Negative Value - 1
- Result: fxp-s33/16(-32769.0)
- float(result): -32769.0
- type(result): fxp-s33/16
numfi
- url: https://github.com/francof2a/fxpmath
- Project name: numfi
- Package name: numfi
- Notes:
from numfi import numfi
# construct
value = numfi(-3, s=True, w=bits, f=frac_bits, rounding='round', overflow='wrap', fixed=True)
testbench(value, type_to_str_f=lambda x: f"{repr(x)}")
a
Result: numfi([-3.]) s32/16-r/w
float(result): -3.0
type(result): numfi([-3.]) s32/16-r/w
/home/simon/.venv3/lib/python3.8/site-packages/numfi/numfi.py:17: UserWarning: n_frac=32 is too large, overflow/underflow may happen during quantization warnings.warn(f"n_frac={n_frac} is too large, overflow/underflow may happen during quantization")
a * a
- Result: numfi([9.]) s32/16-r/w
- float(result): 9.0
- type(result): numfi([9.]) s32/16-r/w
a / a
- Result: numfi([1.]) s32/16-r/w
- float(result): 1.0
- type(result): numfi([1.]) s32/16-r/w
a + a
- Result: numfi([-6.]) s32/16-r/w
- float(result): -6.0
- type(result): numfi([-6.]) s32/16-r/w
a - a
- Result: numfi([0.]) s32/16-r/w
- float(result): 0.0
- type(result): numfi([0.]) s32/16-r/w
a**a
- Result: numfi([-0.03703308]) s32/16-r/w
- float(result): -0.037037037037037035
- type(result): numfi([-0.03703308]) s32/16-r/w
|a|
- Result: numfi([3.]) s32/16-r/w
- float(result): 3.0
- type(result): numfi([3.]) s32/16-r/w
Most Positive Value
- Result: numfi([32767.]) s32/16-r/w
- float(result): 32767.0
- type(result): numfi([32767.]) s32/16-r/w
Most Positive Value + 1
- Result: numfi([-32768.]) s32/16-r/w
- float(result): -32768.0
- type(result): numfi([-32768.]) s32/16-r/w
Most Negative Value
- Result: numfi([-32768.]) s32/16-r/w
- float(result): -32768.0
- type(result): numfi([-32768.]) s32/16-r/w
Most Negative Value - 1
- Result: numfi([32767.]) s32/16-r/w
- float(result): 32767.0
- type(result): numfi([32767.]) s32/16-r/w
fixedpoint
- url: https://github.com/Schweitzer-Engineering-Laboratories/fixedpoint
- Project name: fixedpoint
- Package name: fixedpoint
- Notes: Variable size
from fixedpoint.fixedpoint import FixedPoint
# construct
value = FixedPoint(-3, signed=True, m=int_bits, n=frac_bits, overflow='wrap', rounding='auto')
testbench(value, type_to_str_f=lambda x: f"{repr(x)}")
a
- Result: FixedPoint(‘0xfffd0000’, signed=1, m=16, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): -3.0
- type(result): FixedPoint(‘0xfffd0000’, signed=1, m=16, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
a * a
- Result: FixedPoint(‘0x900000000’, signed=1, m=32, n=32, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): 9.0
- type(result): FixedPoint(‘0x900000000’, signed=1, m=32, n=32, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
a / a
- Result: unsupported operand type(s) for /: ‘FixedPoint’ and ‘FixedPoint’
- type(result): <class ‘TypeError’>
a + a
- Result: FixedPoint(‘0x1fffa0000’, signed=1, m=17, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): -6.0
- type(result): FixedPoint(‘0x1fffa0000’, signed=1, m=17, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
a - a
- Result: FixedPoint(‘0x0’, signed=1, m=17, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): 0.0
- type(result): FixedPoint(‘0x0’, signed=1, m=17, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
a**a
- Result: Only positive integers are supported for exponentiation.
- type(result): <class ‘TypeError’>
|a|
- Result: FixedPoint(‘0x30000’, signed=1, m=16, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): 3.0
- type(result): FixedPoint(‘0x30000’, signed=1, m=16, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
Most Positive Value
- Result: FixedPoint(‘0x7fff0000’, signed=1, m=33, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): 32767.0
- type(result): FixedPoint(‘0x7fff0000’, signed=1, m=33, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
Most Positive Value + 1
- Result: FixedPoint(‘0x80000000’, signed=1, m=34, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): 32768.0
- type(result): FixedPoint(‘0x80000000’, signed=1, m=34, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
Most Negative Value
- Result: FixedPoint(‘0x3ffff80000000’, signed=1, m=34, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): -32768.0
- type(result): FixedPoint(‘0x3ffff80000000’, signed=1, m=34, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
Most Negative Value - 1
- Result: FixedPoint(‘0x7ffff7fff0000’, signed=1, m=35, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
- float(result): -32769.0
- type(result): FixedPoint(‘0x7ffff7fff0000’, signed=1, m=35, n=16, overflow=‘wrap’, rounding=‘convergent’, overflow_alert=‘error’, mismatch_alert=‘warning’, implicit_cast_alert=‘warning’, str_base=16)
fixedpointtest
- url: https://github.com/sixty-north/fixedpointtest
- Project name: fixedpointtest
- Package name: fixedpoint
- Notes: Variable size. Same package name Schweitzer-Engineering-Laboratories/fixedpoint. I changed the package name in setup.py to fixedpoint2 to have these coexist.
from fixedpoint2 import FixedPoint, QFormat
value = FixedPoint(-3, qformat=QFormat(integer_bits=int_bits, fraction_bits=frac_bits))
testbench(value, type_to_str_f=lambda x: f"{repr(x)}")
a
- Result: FixedPoint(-3, QFormat(16, 16))
- float(result): -3.0
- type(result): FixedPoint(-3, QFormat(16, 16))
a * a
- Result: FixedPoint(9, QFormat(33, 32))
- float(result): 9.0
- type(result): FixedPoint(9, QFormat(33, 32))
a / a
- Result: FixedPoint(1, QFormat(33, 32))
- float(result): 1.0
- type(result): FixedPoint(1, QFormat(33, 32))
a + a
- Result: FixedPoint(-6, QFormat(17, 16))
- float(result): -6.0
- type(result): FixedPoint(-6, QFormat(17, 16))
a - a
- Result: FixedPoint(0, QFormat(18, 16))
- float(result): 0.0
- type(result): FixedPoint(0, QFormat(18, 16))
a**a
- Result: FixedPoint(0.037037037037038089692941866815090179443359375, QFormat(51, 46))
- float(result): -0.03703703703703809
- type(result): FixedPoint(0.037037037037038089692941866815090179443359375, QFormat(51, 46))
|a|
- Result: FixedPoint(3, QFormat(16, 16))
- float(result): 3.0
- type(result): FixedPoint(3, QFormat(16, 16))
Most Positive Value
- Result: FixedPoint(32767, QFormat(35, 16))
- float(result): 32767.0
- type(result): FixedPoint(32767, QFormat(35, 16))
Most Positive Value + 1
- Result: FixedPoint(32768, QFormat(36, 16))
- float(result): 32768.0
- type(result): FixedPoint(32768, QFormat(36, 16))
Most Negative Value
- Result: FixedPoint(-32768, QFormat(37, 16))
- float(result): -32768.0
- type(result): FixedPoint(-32768, QFormat(37, 16))
Most Negative Value - 1
- Result: FixedPoint(-32769, QFormat(38, 16))
- float(result): -32769.0
- type(result): FixedPoint(-32769, QFormat(38, 16))