Fixed Point Python Libraries

Evaluating the best library for fixed point algorithm development & testing

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.

LibraryAuto ResizeOverflow HandlingException on OverflowPythonic SyntaxNotes
spfpm❌ NoSaturates at limits✅ Yes✅ YesMost like hardware behavior, strict typing
numfi❌ NoWraparound❌ No✅ Yes
fpbinary✅ YesGrows with operations❌ No✅ YesType expands with each operation
fxpmath✅ YesGrows with operations❌ No✅ YesGood for DSP work, limited abs support
fixedpoint✅ YesAuto adjusts size❌ No✅ Yes
fixedpointtest✅ YesAuto adjusts size❌ No✅ YesMore 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

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

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

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

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

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))

Other Libraries