Repository: mpmath/mpmath (python)

Issue:
Add mpf.__format__() dunder methods
I am struggling with the conversion of a script written for Python 2.7 to Python 3 (PS: I know Python 3 still understands the old formatting syntax, but I would like to convert to the new formatting syntax as well). Consider the following example (IPython with output):

    In [1]: import mpmath
    
    In [2]: value = mpmath.mp.mpf('1.0')
    
    In [3]: print('%10.0f' % value)
             1
    
    In [4]: print('{:10.0f}'.format(value))
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-4-f8a6817daeeb> in <module>()
    ----> 1 print('{:10.0f}'.format(value))

Is there a sensible fix for this?

I have tried:

     print('{:10s}'.format(mpfloat.nstr(value)))

instead, but I cannot seem to figure out how to configure `nstr` to behave like line 3 above. The latter seems to perform rounding of some numbers that the former does not...

Pull request description:
Implement mpf.__format__
Implemented the `__format__` method for the `_mpf` type.

Currently, `f`, `F`, `g`, `G`, `e`, and `E` formats are implemented, following the same specifications as specified for regular floats [here](https://docs.python.org/3/library/string.html#formatspec).
Only nearest rounding is implemented.

Partially resolves #337.

Patch:
diff --git a/CHANGES b/CHANGES
index 4df3466..8cf0fc4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -878,7 +878,7 @@ New features:
 
 Bugfixes, speed enhancements, and other improvements:
 * compatibility tests should now pass on systems where Python is
-  compiled to use 80-bit registers for floating point operations
+  compiled to use 80-bit registers for floating-point operations
 * fixed mpmath to work with some versions of Python 2.4 where a
   list indexing bug is present in the Python core
 * better algorithms for various complex elementary functions
diff --git a/docs/matrices.rst b/docs/matrices.rst
index 2833c11..6d9e131 100644
--- a/docs/matrices.rst
+++ b/docs/matrices.rst
@@ -293,7 +293,8 @@ using ``lu_solve``::
     [['30.0'],
      ['-20.0']])
 
-If you don't trust the result, use ``residual`` to calculate the residual ||A*x-b||::
+If you don't trust the result, use ``residual`` to calculate
+the residual `||A x-b||`::
 
     >>> residual(A, x, b)
     matrix(
@@ -303,7 +304,7 @@ If you don't trust the result, use ``residual`` to calculate the residual ||A*x-
     '2.22044604925031e-16'
 
 As you can see, the solution is quite accurate. The error is caused by the
-inaccuracy of the internal floating point arithmetic. Though, it's even smaller
+inaccuracy of the internal floating-point arithmetic. Though, it's even smaller
 than the current machine epsilon, which basically means you can trust the
 result.
 
diff --git a/mpmath/ctx_mp.py b/mpmath/ctx_mp.py
index ee05350..41e2870 100644
--- a/mpmath/ctx_mp.py
+++ b/mpmath/ctx_mp.py
@@ -904,7 +904,7 @@ def fsub(ctx, x, y, **kwargs):
             >>> print(fsub(x, y, exact=True) + y)
             2.0
 
-        Exact addition can be inefficient and may be impossible to perform
+        Exact subtraction can be inefficient and may be impossible to perform
         with large magnitude differences::
 
             >>> fsub(1, '1e-100000000000000000000', prec=inf)
diff --git a/mpmath/ctx_mp_python.py b/mpmath/ctx_mp_python.py
index 8259ab9..e1a7bf6 100644
--- a/mpmath/ctx_mp_python.py
+++ b/mpmath/ctx_mp_python.py
@@ -1,11 +1,12 @@
 import numbers
+import sys
 
 from . import function_docs
 from .libmp import (MPQ, MPZ, ComplexResult, dps_to_prec, finf, fnan, fninf,
-                    from_Decimal, from_float, from_int, from_man_exp,
-                    from_npfloat, from_rational, from_str, fzero, int_types,
-                    mpc_abs, mpc_add, mpc_add_mpf, mpc_conjugate, mpc_div,
-                    mpc_div_mpf, mpc_hash, mpc_is_inf, mpc_is_nonzero,
+                    format_mpf, from_Decimal, from_float, from_int,
+                    from_man_exp, from_npfloat, from_rational, from_str, fzero,
+                    int_types, mpc_abs, mpc_add, mpc_add_mpf, mpc_conjugate,
+                    mpc_div, mpc_div_mpf, mpc_hash, mpc_is_inf, mpc_is_nonzero,
                     mpc_mpf_div, mpc_mpf_sub, mpc_mul, mpc_mul_int,
                     mpc_mul_mpf, mpc_neg, mpc_pos, mpc_pow, mpc_pow_int,
                     mpc_pow_mpf, mpc_sub, mpc_sub_mpf, mpc_to_complex,
@@ -354,6 +355,9 @@ def __rpow__(s, t):
             return t
         return t ** s
 
+    def __format__(s, format_spec):
+        return format_mpf(s._mpf_, format_spec)
+
     def sqrt(s):
         return s.context.sqrt(s)
 
@@ -610,7 +614,7 @@ def ae(s, t, rel_eps=None, abs_eps=None):
 
 class PythonMPContext:
     def __init__(ctx):
-        ctx._prec_rounding = [53, round_nearest]
+        ctx._prec_rounding = [sys.float_info.mant_dig, round_nearest]
         ctx.mpf = type('mpf', (_mpf,), {})
         ctx.mpc = type('mpc', (_mpc,), {})
         ctx.mpf._ctxdata = [ctx.mpf, new, ctx._prec_rounding]
@@ -632,8 +636,8 @@ def make_mpc(ctx, v):
         return a
 
     def default(ctx):
-        ctx._prec = ctx._prec_rounding[0] = 53
-        ctx._dps = 15
+        ctx._prec = ctx._prec_rounding[0] = sys.float_info.mant_dig
+        ctx._dps = sys.float_info.dig
         ctx.trap_complex = False
 
     def _set_prec(ctx, n):
diff --git a/mpmath/libmp/__init__.py b/mpmath/libmp/__init__.py
index fd236d7..b6da7f0 100644
--- a/mpmath/libmp/__init__.py
+++ b/mpmath/libmp/__init__.py
@@ -43,15 +43,15 @@
                      mpc_tanh, mpc_to_complex, mpc_to_str, mpc_two, mpc_zero,
                      mpf_expj, mpf_expjpi)
 from .libmpf import (ComplexResult, dps_to_prec, fhalf, finf, fnan, fninf,
-                     fnone, fnzero, fone, from_Decimal, from_float, from_int,
-                     from_man_exp, from_npfloat, from_pickable, from_rational,
-                     from_str, ften, ftwo, fzero, mpf_abs, mpf_add, mpf_ceil,
-                     mpf_cmp, mpf_div, mpf_eq, mpf_floor, mpf_frac, mpf_frexp,
-                     mpf_ge, mpf_gt, mpf_hash, mpf_hypot, mpf_le, mpf_lt,
-                     mpf_mod, mpf_mul, mpf_mul_int, mpf_neg, mpf_nint,
-                     mpf_perturb, mpf_pos, mpf_pow_int, mpf_rand, mpf_rdiv_int,
-                     mpf_shift, mpf_sign, mpf_sqrt, mpf_sub, mpf_sum,
-                     normalize, prec_to_dps, repr_dps, round_ceiling,
+                     fnone, fnzero, fone, format_mpf, from_Decimal, from_float,
+                     from_int, from_man_exp, from_npfloat, from_pickable,
+                     from_rational, from_str, ften, ftwo, fzero, mpf_abs,
+                     mpf_add, mpf_ceil, mpf_cmp, mpf_div, mpf_eq, mpf_floor,
+                     mpf_frac, mpf_frexp, mpf_ge, mpf_gt, mpf_hash, mpf_hypot,
+                     mpf_le, mpf_lt, mpf_mod, mpf_mul, mpf_mul_int, mpf_neg,
+                     mpf_nint, mpf_perturb, mpf_pos, mpf_pow_int, mpf_rand,
+                     mpf_rdiv_int, mpf_shift, mpf_sign, mpf_sqrt, mpf_sub,
+                     mpf_sum, normalize, prec_to_dps, repr_dps, round_ceiling,
                      round_down, round_floor, round_int, round_nearest,
                      round_up, str_to_man_exp, to_digits_exp, to_fixed,
                      to_float, to_int, to_man_exp, to_pickable, to_rational,
diff --git a/mpmath/libmp/libmpf.py b/mpmath/libmp/libmpf.py
index f39f6b2..9054b6a 100644
--- a/mpmath/libmp/libmpf.py
+++ b/mpmath/libmp/libmpf.py
@@ -3,6 +3,7 @@
 """
 
 import math
+import re
 import sys
 import warnings
 
@@ -1101,6 +1102,50 @@ def to_digits_exp(s, dps, base=10):
     exponent += len(digits) - fixdps - 1
     return sign, digits, exponent
 
+def round_digits(digits, dps, base):
+    '''
+    Returns the rounded digits, and the number of places the decimal point was
+    shifted.
+    '''
+
+    assert len(digits) > dps
+    exponent = 0
+
+    rnd_digs = stddigits[(base//2 + base % 2):base]
+
+    round_up = False
+    # The first digit after dps is a 5.
+    if digits[dps] == rnd_digs[0]:
+        for i in range(dps+1, len(digits)):
+            if digits[i] != '0':
+                round_up = True
+                break
+        if digits[dps-1] in stddigits[1:base:2]:
+            round_up = True
+
+    if not round_up:
+        digits = digits[:dps] + stddigits[int(digits[dps], base) - 1]
+
+    # Rounding up kills some instances of "...99999"
+    if digits[dps] in rnd_digs:
+        digits = digits[:dps]
+        i = dps - 1
+        dig = stddigits[base-1]
+        while i >= 0 and digits[i] == dig:
+            i -= 1
+        if i >= 0:
+            digits = digits[:i] + stddigits[int(digits[i], base) + 1] + \
+                '0' * (dps - i - 1)
+        else:
+            digits = '1' + '0' * (dps - 1)
+            exponent += 1
+    else:
+        digits = digits[:dps]
+
+    return digits, exponent
+
+
+
 def to_str(s, dps, strip_zeros=True, min_fixed=None, max_fixed=None,
            show_zero_exponent=False, base=10, binary_exp=False):
     """
@@ -1159,7 +1204,7 @@ def to_str(s, dps, strip_zeros=True, min_fixed=None, max_fixed=None,
 
     # to_digits_exp rounds to floor.
     # This sometimes kills some instances of "...00001"
-    sign, digits, exponent = to_digits_exp(s, dps+3, base)
+    sign, digits, exponent = to_digits_exp(s, dps+10, base)
 
     rnd_digs = stddigits[(base//2 + base%2):base]
 
@@ -1311,6 +1356,298 @@ def from_str(x, prec=0, rnd=round_fast, base=0):
     return s
 
 
+#----------------------------------------------------------------------------#
+#                              String formatting                             #
+#----------------------------------------------------------------------------#
+
+blog2_10 = 3.3219280948873626
+
+_FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
+    (?:
+        (?P<fill_char>.)?
+        (?P<align>[<>=^])
+    )?
+    (?P<sign>[-+ ]?)
+    (?P<no_neg_0>z)?
+    (?P<alternate>\#)?
+    # A '0' that's *not* followed by another digit is parsed as a minimum width
+    # rather than a zeropad flag.
+    (?P<zeropad>0(?=[0-9]))?
+    (?P<width>0|[1-9][0-9]*)?
+    (?P<thousands_separators>[,_])?
+    (?:\.(?P<precision>0|[1-9][0-9]*))?
+    (?P<type>[eEfFgG])
+""", re.DOTALL | re.VERBOSE).fullmatch
+
+
+def calc_padding(nchars, width, align):
+    '''
+    Computes the left and right padding required to fill the required width,
+    according to how the string will be aligned.
+    '''
+    ntotal = max(nchars, width)
+
+    if align in ('>', '='):
+        lpad = ntotal - nchars
+        rpad = 0
+    elif align == '^':
+        lpad = (ntotal - nchars)//2
+        rpad = ntotal - nchars - lpad
+    else:
+        lpad = 0
+        rpad = ntotal - nchars
+
+    return (lpad, rpad)
+
+
+def read_format_spec(format_spec):
+    '''
+    Reads the format spec into a dictionary.
+    This is more or less copied from the CPython implementation for regular
+    floats.
+    '''
+
+    format_dict = {
+        'fill_char': ' ',
+        'align': '>',
+        'sign': '-',
+        'no_neg_0': False,
+        'alternate': False,
+        'thousands_separators': '',
+        'width': -1,
+        'precision': 6,
+        'type': 'f'
+        }
+
+    if match := _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec):
+        format_dict['fill_char'] = match['fill_char'] or format_dict['fill_char']
+        format_dict['align'] = match['align'] or format_dict['align']
+        format_dict['sign'] = match['sign'] or format_dict['sign']
+        format_dict['no_neg_0'] = bool(match['no_neg_0']) or format_dict['no_neg_0']
+        format_dict['alternate'] = bool(match['alternate']) or \
+            format_dict['alternate']
+        format_dict['thousands_separators'] = match['thousands_separators'] \
+            or format_dict['thousands_separators']
+        format_dict['width'] = int(match['width'] or format_dict['width'])
+        format_dict['precision'] = int(match['precision'] or format_dict['precision'])
+        format_dict['type'] = match['type'] or format_dict['type']
+
+        if match['zeropad'] and match['fill_char']:
+            raise ValueError('Cannot specify both 0-padding and a fill '
+                             'character')
+
+        if match['zeropad']:
+            format_dict['align'] = '='
+            format_dict['fill_char'] = '0'
+    else:
+        raise ValueError("Invalid format specifier '{}'".format(format_spec))
+
+    return format_dict
+
+
+def format_fixed(s,
+                 precision=6,
+                 strip_zeros=False,
+                 strip_last_zero=False,
+                 thousands_separators='',
+                 sign_spec='-',
+                 sep_range=3,
+                 base=10,
+                 alternate=False,
+                 no_neg_0=False):
+    '''
+    Format a number into fixed point.
+    Returns the sign character, and the string that represents the number in
+    the correct format.
+    Does not perform padding or aligning
+    '''
+
+    # First, get the exponent to know how many digits we will need
+    _, _, exponent = to_digits_exp(s, 1, base)
+
+    # Now that we have an estimate, compute the correct digits
+    # (we do this because the previous computation could yield the wrong
+    # exponent by +- 1)
+    sign, digits, exponent = to_digits_exp(
+            s, max(precision+exponent+4, int(s[3]/blog2_10)), base)
+    dps = precision + exponent + 1
+
+    # Hack: if the digits are all 9s, then we will lose one dps when rounding
+    # up.
+    if all(dig == stddigits[base-1] for dig in digits[:dps+1]):
+        dps += 1
+
+    if sign != '-' and sign_spec != '-':
+        sign = sign_spec
+
+    # The number we want to print is lower in magnitude that the requested
+    # precision. We should only print 0s.
+    if exponent + precision < -1:
+        if precision > 0:
+            digits = '0.' + precision*'0'
+        else:
+            digits = '0'
+
+        if no_neg_0:
+            sign = '' if sign_spec == '-' else sign_spec
+    else:
+        digits, exp_add = round_digits(digits, dps, base)
+        exponent += exp_add
+
+        # Here we prepend the corresponding 0s to the digits string, according
+        # to the value of exponent
+        if exponent < 0:
+            digits = ("0"*(-exponent)) + digits
+            split = 1
+        else:
+            split = exponent + 1
+        exponent = 0
+
+        # Add the thousands separator every 3 characters.
+        if thousands_separators != '' and split > sep_range:
+            # the first thousand separator may be located before 3 characters
+            nmod = split % sep_range
+            digs_b = digits[nmod:split]
+
+            if nmod != 0:
+                prev = digits[:nmod] + thousands_separators
+            else:
+                prev = ''
+
+            dec_part = prev + thousands_separators.join(
+                    digs_b[i:i+sep_range]
+                    for i in range(0, split-nmod, sep_range)
+                    )
+        else:
+            dec_part = digits[:split]
+
+        # Finally, assemble the digits including the decimal point
+        if precision == 0:
+            return sign, dec_part + ('.' if alternate else '')
+
+        digits = dec_part + "." + digits[split:]
+
+    if strip_zeros:
+        # Clean up trailing zeros
+        digits = digits.rstrip('0')
+
+    if digits[-1] == "." and strip_last_zero:
+        digits = digits[:-1]
+
+    return sign, digits
+
+
+def format_scientific(s,
+                      precision=6,
+                      strip_zeros=False,
+                      sign_spec='-',
+                      base=10,
+                      capitalize=False,
+                      alternate=False,
+                      no_neg_0=False):
+
+    sep = 'E' if capitalize else 'e'
+
+    # First, get the exponent to know how many digits we will need
+    dps = precision+1
+    sign, digits, exponent = to_digits_exp(
+            s, max(dps+10, int(s[3]/blog2_10)+10), base)
+
+    if sign != '-' and sign_spec != '-':
+        sign = sign_spec
+
+    digits, exp_add = round_digits(digits, dps, base)
+    exponent += exp_add
+
+    if strip_zeros:
+        # Clean up trailing zeros
+        digits = digits.rstrip('0')
+        precision = len(digits)
+
+    if precision >= 1 and len(digits) > 1:
+        return sign, digits[0] + '.' + digits[1:] + sep + f'{exponent:+03d}'
+
+    if alternate:
+        return sign, digits + '.' + sep + f'{exponent:+03d}'
+
+    return sign, digits + sep + f'{exponent:+03d}'
+
+
+def format_mpf(num, format_spec):
+    format_dict = read_format_spec(format_spec)
+
+    capitalize = False
+    if format_dict['type'] in 'FGE':
+        capitalize = True
+
+    fmt_type = format_dict['type'].lower()
+    precision = format_dict['precision']
+
+    digits = ''
+    sign = ''
+
+    # Special cases:
+    if num in (fnan, finf, fninf, fzero, fnzero):
+        return format(to_float(num), format_spec)
+
+    # Now the general case
+    strip_last_zero = False
+    strip_zeros = False
+
+    if fmt_type == 'g':
+        if not format_dict['alternate']:
+            strip_last_zero = True
+            strip_zeros = True
+
+        _, _, exp = to_digits_exp(num, 53/blog2_10, 10)
+        if precision == 0:
+            precision = 1
+
+        if -4 <= exp < precision:
+            fmt_type = 'f'
+            precision = max(0, precision - exp - 1)
+        else:
+            fmt_type = 'e'
+            precision = max(0, precision - 1)
+
+    if fmt_type == 'f':
+        sign, digits = format_fixed(
+                num,
+                precision=precision,
+                strip_zeros=strip_zeros,
+                strip_last_zero=strip_last_zero,
+                thousands_separators=format_dict['thousands_separators'],
+                sign_spec=format_dict['sign'],
+                sep_range=3,
+                base=10,
+                alternate=format_dict['alternate'],
+                no_neg_0=format_dict['no_neg_0']
+                )
+    else:  # The format type is scientific
+        sign, digits = format_scientific(
+                num,
+                precision=precision,
+                strip_zeros=strip_zeros,
+                sign_spec=format_dict['sign'],
+                base=10,
+                capitalize=capitalize,
+                alternate=format_dict['alternate'],
+                no_neg_0=format_dict['no_neg_0']
+                )
+
+    nchars = len(digits) + len(sign)
+
+    lpad, rpad = calc_padding(
+            nchars, format_dict['width'], format_dict['align'])
+
+    if format_dict['align'] == '=':
+        return sign + lpad*format_dict['fill_char'] + digits + \
+                rpad*format_dict['fill_char']
+
+    return lpad*format_dict['fill_char'] + sign + digits \
+            + rpad*format_dict['fill_char']
+
+
 #----------------------------------------------------------------------------#
 #                                Square roots                                #
 #----------------------------------------------------------------------------#
diff --git a/mpmath/matrices/eigen.py b/mpmath/matrices/eigen.py
index 24230e0..38a9c32 100644
--- a/mpmath/matrices/eigen.py
+++ b/mpmath/matrices/eigen.py
@@ -86,7 +86,7 @@ def hessenberg_reduce_0(ctx, A, T):
             scale_inv = 1 / scale
 
         if scale == 0 or ctx.isinf(scale_inv):
-            # sadly there are floating point numbers not equal to zero whose reciprocal is infinity
+            # sadly there are floating-point numbers not equal to zero whose reciprocal is infinity
             T[i] = 0
             A[i,i-1] = 0
             continue
diff --git a/mpmath/matrices/eigen_symmetric.py b/mpmath/matrices/eigen_symmetric.py
index 716b2a8..b691d69 100644
--- a/mpmath/matrices/eigen_symmetric.py
+++ b/mpmath/matrices/eigen_symmetric.py
@@ -85,7 +85,7 @@ def r_sy_tridiag(ctx, A, D, E, calc_ev = True):
         if scale != 0:
             scale_inv = 1/scale
 
-        # sadly there are floating point numbers not equal to zero whose reciprocal is infinity
+        # sadly there are floating-point numbers not equal to zero whose reciprocal is infinity
 
         if i == 1 or scale == 0 or ctx.isinf(scale_inv):
             E[i] = A[i-1,i]        # nothing to do
@@ -211,7 +211,7 @@ def c_he_tridiag_0(ctx, A, D, E, T):
         if scale != 0:
             scale_inv = 1 / scale
 
-        # sadly there are floating point numbers not equal to zero whose reciprocal is infinity
+        # sadly there are floating-point numbers not equal to zero whose reciprocal is infinity
 
         if scale == 0 or ctx.isinf(scale_inv):
             E[i] = 0
diff --git a/mpmath/matrices/linalg.py b/mpmath/matrices/linalg.py
index f4012a3..ddde1c2 100644
--- a/mpmath/matrices/linalg.py
+++ b/mpmath/matrices/linalg.py
@@ -32,7 +32,7 @@
     '2.22044604925031e-16'
 
 As you can see, the solution is quite accurate. The error is caused by the
-inaccuracy of the internal floating point arithmetic. Though, it's even smaller
+inaccuracy of the internal floating-point arithmetic. Though, it's even smaller
 than the current machine epsilon, which basically means you can trust the
 result.
 


Test patch:
diff --git a/mpmath/tests/test_format.py b/mpmath/tests/test_format.py
new file mode 100644
index 0000000..e208d1b
--- /dev/null
+++ b/mpmath/tests/test_format.py
@@ -0,0 +1,577 @@
+import random
+import sys
+
+import pytest
+
+from mpmath import fp, inf, mp, nan, ninf, workdps
+
+
+def random_fmt():
+    '''
+    This helper generates valid random format strings.
+    '''
+
+    fmt_str = '{:'
+
+    # fill_char and align
+    n = random.randint(0, 2)
+    if n == 0:
+        fmt_str += random.choice('z;clxvjqwer') + random.choice('<^>=')
+        skip_0_padding = True
+    elif n == 1:
+        fmt_str += random.choice('<^>=')
+        skip_0_padding = True
+    else:
+        skip_0_padding = False
+
+    # sign character
+    n = random.randint(0, 1)
+    if n == 1:
+        fmt_str += random.choice('-+ ')
+
+    # no_neg_0 (not used yet.)
+    if sys.version_info[:3] > (3, 11):
+        n = random.randint(0, 1)
+        if n == 1:
+            fmt_str += 'z'
+
+    # alternate mode
+    n = random.randint(0, 1)
+    if n == 1:
+        fmt_str += '#'
+
+    # pad with 0s
+    n = random.randint(0, 1)
+    skip_thousand_separators = False
+    if n == 1 and not skip_0_padding:
+        fmt_str += '0'
+        skip_thousand_separators = True
+
+    # Width
+    n = random.randint(0, 2)
+    if n > 0:
+        fmt_str += str(random.randint(1, 40))
+
+    # grouping character (thousand_separators)
+    n = random.randint(0, 1)
+    if n == 1 and not skip_thousand_separators:
+        fmt_str += random.choice(',_')
+
+    # Precision
+    n = random.randint(0, 2)
+    if n > 0:
+        fmt_str += '.' + str(random.randint(1, 40))
+
+    # Type
+    fmt_str += random.choice('fFgGeE')
+    fmt_str += '}'
+
+    return fmt_str
+
+
+def test_mpf_fmt_cpython():
+    '''
+    These tests assure that mpf.__format__ yields the same result as regular
+    float.__format__, when dps is default.
+    '''
+
+    # zeros
+    assert f'{mp.mpf(0):.0f}' == '0'
+    assert f'{mp.mpf(0):.1f}' == '0.0'
+    assert f'{mp.mpf(0):.2f}' == '0.00'
+    assert f'{mp.mpf(0):.3f}' == '0.000'
+    assert f'{mp.mpf(0):.50f}' == '0.00000000000000000000000000000000000000000000000000'
+
+    # precision 0;  result should never include a .
+    assert f'{mp.mpf(1.5):.0f}' == '2'
+    assert f'{mp.mpf(2.5):.0f}' == '2'
+    assert f'{mp.mpf(3.5):.0f}' == '4'
+    assert f'{mp.mpf(0.0):.0f}' == '0'
+    assert f'{mp.mpf(0.1):.0f}' == '0'
+    assert f'{mp.mpf(0.001):.0f}' == '0'
+    assert f'{mp.mpf(10.0):.0f}' == '10'
+    assert f'{mp.mpf(10.1):.0f}' == '10'
+    assert f'{mp.mpf(10.01):.0f}' == '10'
+    assert f'{mp.mpf(123.456):.0f}' == '123'
+    assert f'{mp.mpf(1234.56):.0f}' == '1235'
+    assert f'{mp.mpf(1e49):.0f}' == '9999999999999999464902769475481793196872414789632'
+    assert f'{mp.mpf(9.9999999999999987e+49):.0f}' == '99999999999999986860582406952576489172979654066176'
+    assert f'{mp.mpf(1e50):.0f}' == '100000000000000007629769841091887003294964970946560'
+
+    # precision 1
+    assert f'{mp.mpf(0.0001):.1f}' == '0.0'
+    assert f'{mp.mpf(0.001):.1f}' == '0.0'
+    assert f'{mp.mpf(0.01):.1f}' == '0.0'
+    assert f'{mp.mpf(0.04):.1f}' == '0.0'
+    assert f'{mp.mpf(0.06):.1f}' == '0.1'
+    assert f'{mp.mpf(0.25):.1f}' == '0.2'
+    assert f'{mp.mpf(0.75):.1f}' == '0.8'
+    assert f'{mp.mpf(1.4):.1f}' == '1.4'
+    assert f'{mp.mpf(1.5):.1f}' == '1.5'
+    assert f'{mp.mpf(10.0):.1f}' == '10.0'
+    assert f'{mp.mpf(1000.03):.1f}' == '1000.0'
+    assert f'{mp.mpf(1234.5678):.1f}' == '1234.6'
+    assert f'{mp.mpf(1234.7499):.1f}' == '1234.7'
+    assert f'{mp.mpf(1234.75):.1f}' == '1234.8'
+
+    # precision 2
+    assert f'{mp.mpf(0.0001):.2f}' == '0.00'
+    assert f'{mp.mpf(0.001):.2f}' == '0.00'
+    assert f'{mp.mpf(0.004999):.2f}' == '0.00'
+    assert f'{mp.mpf(0.005001):.2f}' == '0.01'
+    assert f'{mp.mpf(0.01):.2f}' == '0.01'
+    assert f'{mp.mpf(0.125):.2f}' == '0.12'
+    assert f'{mp.mpf(0.375):.2f}' == '0.38'
+    assert f'{mp.mpf(1234500):.2f}' == '1234500.00'
+    assert f'{mp.mpf(1234560):.2f}' == '1234560.00'
+    assert f'{mp.mpf(1234567):.2f}' == '1234567.00'
+    assert f'{mp.mpf(1234567.8):.2f}' == '1234567.80'
+    assert f'{mp.mpf(1234567.89):.2f}' == '1234567.89'
+    assert f'{mp.mpf(1234567.891):.2f}' == '1234567.89'
+    assert f'{mp.mpf(1234567.8912):.2f}' == '1234567.89'
+
+    # alternate form always includes a decimal point.  This only
+    # makes a difference when the precision is 0.
+    assert f'{mp.mpf(0):#.0f}' == '0.'
+    assert f'{mp.mpf(0):#.1f}' == '0.0'
+    assert f'{mp.mpf(1.5):#.0f}' == '2.'
+    assert f'{mp.mpf(2.5):#.0f}' == '2.'
+    assert f'{mp.mpf(10.1):#.0f}' == '10.'
+    assert f'{mp.mpf(1234.56):#.0f}' == '1235.'
+    assert f'{mp.mpf(1.4):#.1f}' == '1.4'
+    assert f'{mp.mpf(0.375):#.2f}' == '0.38'
+
+    # if precision is omitted it defaults to 6
+    assert f'{mp.mpf(0):f}' == '0.000000'
+    assert f'{mp.mpf(1230000):f}' == '1230000.000000'
+    assert f'{mp.mpf(1234567):f}' == '1234567.000000'
+    assert f'{mp.mpf(123.4567):f}' == '123.456700'
+    assert f'{mp.mpf(1.23456789):f}' == '1.234568'
+    assert f'{mp.mpf(0.00012):f}' == '0.000120'
+    assert f'{mp.mpf(0.000123):f}' == '0.000123'
+    assert f'{mp.mpf(0.00012345):f}' == '0.000123'
+    assert f'{mp.mpf(0.000001):f}' == '0.000001'
+    assert f'{mp.mpf(0.0000005001):f}' == '0.000001'
+    assert f'{mp.mpf(0.0000004999):f}' == '0.000000'
+
+    # 'e' code formatting with explicit precision (>= 0). Output should
+    # always have exactly the number of places after the point that were
+    # requested.
+
+    # zeros
+    assert f'{mp.mpf(0):.0e}' == '0e+00'
+    assert f'{mp.mpf(0):.1e}' == '0.0e+00'
+    assert f'{mp.mpf(0):.2e}' == '0.00e+00'
+    assert f'{mp.mpf(0):.10e}' == '0.0000000000e+00'
+    assert f'{mp.mpf(0):.50e}' == '0.00000000000000000000000000000000000000000000000000e+00'
+
+    # precision 0.  no decimal point in the output
+    assert f'{mp.mpf(0.01):.0e}' == '1e-02'
+    assert f'{mp.mpf(0.1):.0e}' == '1e-01'
+    assert f'{mp.mpf(1):.0e}' == '1e+00'
+    assert f'{mp.mpf(10):.0e}' == '1e+01'
+    assert f'{mp.mpf(100):.0e}' == '1e+02'
+    assert f'{mp.mpf(0.012):.0e}' == '1e-02'
+    assert f'{mp.mpf(0.12):.0e}' == '1e-01'
+    assert f'{mp.mpf(1.2):.0e}' == '1e+00'
+    assert f'{mp.mpf(12):.0e}' == '1e+01'
+    assert f'{mp.mpf(120):.0e}' == '1e+02'
+    assert f'{mp.mpf(123.456):.0e}' == '1e+02'
+    assert f'{mp.mpf(0.000123456):.0e}' == '1e-04'
+    assert f'{mp.mpf(123456000):.0e}' == '1e+08'
+    assert f'{mp.mpf(0.5):.0e}' == '5e-01'
+    assert f'{mp.mpf(1.4):.0e}' == '1e+00'
+    assert f'{mp.mpf(1.5):.0e}' == '2e+00'
+    assert f'{mp.mpf(1.6):.0e}' == '2e+00'
+    assert f'{mp.mpf(2.4999999):.0e}' == '2e+00'
+    assert f'{mp.mpf(2.5):.0e}' == '2e+00'
+    assert f'{mp.mpf(2.5000001):.0e}' == '3e+00'
+    assert f'{mp.mpf(3.499999999999):.0e}' == '3e+00'
+    assert f'{mp.mpf(3.5):.0e}' == '4e+00'
+    assert f'{mp.mpf(4.5):.0e}' == '4e+00'
+    assert f'{mp.mpf(5.5):.0e}' == '6e+00'
+    assert f'{mp.mpf(6.5):.0e}' == '6e+00'
+    assert f'{mp.mpf(7.5):.0e}' == '8e+00'
+    assert f'{mp.mpf(8.5):.0e}' == '8e+00'
+    assert f'{mp.mpf(9.4999):.0e}' == '9e+00'
+    assert f'{mp.mpf(9.5):.0e}' == '1e+01'
+    assert f'{mp.mpf(10.5):.0e}' == '1e+01'
+    assert f'{mp.mpf(14.999):.0e}' == '1e+01'
+    assert f'{mp.mpf(15):.0e}' == '2e+01'
+
+    # precision 1
+    assert f'{mp.mpf(0.0001):.1e}' == '1.0e-04'
+    assert f'{mp.mpf(0.001):.1e}' == '1.0e-03'
+    assert f'{mp.mpf(0.01):.1e}' == '1.0e-02'
+    assert f'{mp.mpf(0.1):.1e}' == '1.0e-01'
+    assert f'{mp.mpf(1):.1e}' == '1.0e+00'
+    assert f'{mp.mpf(10):.1e}' == '1.0e+01'
+    assert f'{mp.mpf(100):.1e}' == '1.0e+02'
+    assert f'{mp.mpf(120):.1e}' == '1.2e+02'
+    assert f'{mp.mpf(123):.1e}' == '1.2e+02'
+    assert f'{mp.mpf(123.4):.1e}' == '1.2e+02'
+
+    # precision 2
+    assert f'{mp.mpf(0.00013):.2e}' == '1.30e-04'
+    assert f'{mp.mpf(0.000135):.2e}' == '1.35e-04'
+    assert f'{mp.mpf(0.0001357):.2e}' == '1.36e-04'
+    assert f'{mp.mpf(0.0001):.2e}' == '1.00e-04'
+    assert f'{mp.mpf(0.001):.2e}' == '1.00e-03'
+    assert f'{mp.mpf(0.01):.2e}' == '1.00e-02'
+    assert f'{mp.mpf(0.1):.2e}' == '1.00e-01'
+    assert f'{mp.mpf(1):.2e}' == '1.00e+00'
+    assert f'{mp.mpf(10):.2e}' == '1.00e+01'
+    assert f'{mp.mpf(100):.2e}' == '1.00e+02'
+    assert f'{mp.mpf(1000):.2e}' == '1.00e+03'
+    assert f'{mp.mpf(1500):.2e}' == '1.50e+03'
+    assert f'{mp.mpf(1590):.2e}' == '1.59e+03'
+    assert f'{mp.mpf(1598):.2e}' == '1.60e+03'
+    assert f'{mp.mpf(1598.7):.2e}' == '1.60e+03'
+    assert f'{mp.mpf(1598.76):.2e}' == '1.60e+03'
+    assert f'{mp.mpf(9999):.2e}' == '1.00e+04'
+
+    # omitted precision defaults to 6
+    assert f'{mp.mpf(0):e}' == '0.000000e+00'
+    assert f'{mp.mpf(165):e}' == '1.650000e+02'
+    assert f'{mp.mpf(1234567):e}' == '1.234567e+06'
+    assert f'{mp.mpf(12345678):e}' == '1.234568e+07'
+    assert f'{mp.mpf(1.1):e}' == '1.100000e+00'
+
+    # alternate form always contains a decimal point.  This only makes
+    # a difference when precision is 0.
+
+    assert f'{mp.mpf(0.01):#.0e}' == '1.e-02'
+    assert f'{mp.mpf(0.1):#.0e}' == '1.e-01'
+    assert f'{mp.mpf(1):#.0e}' == '1.e+00'
+    assert f'{mp.mpf(10):#.0e}' == '1.e+01'
+    assert f'{mp.mpf(100):#.0e}' == '1.e+02'
+    assert f'{mp.mpf(0.012):#.0e}' == '1.e-02'
+    assert f'{mp.mpf(0.12):#.0e}' == '1.e-01'
+    assert f'{mp.mpf(1.2):#.0e}' == '1.e+00'
+    assert f'{mp.mpf(12):#.0e}' == '1.e+01'
+    assert f'{mp.mpf(120):#.0e}' == '1.e+02'
+    assert f'{mp.mpf(123.456):#.0e}' == '1.e+02'
+    assert f'{mp.mpf(0.000123456):#.0e}' == '1.e-04'
+    assert f'{mp.mpf(123456000):#.0e}' == '1.e+08'
+    assert f'{mp.mpf(0.5):#.0e}' == '5.e-01'
+    assert f'{mp.mpf(1.4):#.0e}' == '1.e+00'
+    assert f'{mp.mpf(1.5):#.0e}' == '2.e+00'
+    assert f'{mp.mpf(1.6):#.0e}' == '2.e+00'
+    assert f'{mp.mpf(2.4999999):#.0e}' == '2.e+00'
+    assert f'{mp.mpf(2.5):#.0e}' == '2.e+00'
+    assert f'{mp.mpf(2.5000001):#.0e}' == '3.e+00'
+    assert f'{mp.mpf(3.499999999999):#.0e}' == '3.e+00'
+    assert f'{mp.mpf(3.5):#.0e}' == '4.e+00'
+    assert f'{mp.mpf(4.5):#.0e}' == '4.e+00'
+    assert f'{mp.mpf(5.5):#.0e}' == '6.e+00'
+    assert f'{mp.mpf(6.5):#.0e}' == '6.e+00'
+    assert f'{mp.mpf(7.5):#.0e}' == '8.e+00'
+    assert f'{mp.mpf(8.5):#.0e}' == '8.e+00'
+    assert f'{mp.mpf(9.4999):#.0e}' == '9.e+00'
+    assert f'{mp.mpf(9.5):#.0e}' == '1.e+01'
+    assert f'{mp.mpf(10.5):#.0e}' == '1.e+01'
+    assert f'{mp.mpf(14.999):#.0e}' == '1.e+01'
+    assert f'{mp.mpf(15):#.0e}' == '2.e+01'
+    assert f'{mp.mpf(123.4):#.1e}' == '1.2e+02'
+    assert f'{mp.mpf(0.0001357):#.2e}' == '1.36e-04'
+
+    # 'g' code formatting.
+
+    # zeros
+    assert f'{mp.mpf(0):.0g}' == '0'
+    assert f'{mp.mpf(0):.1g}' == '0'
+    assert f'{mp.mpf(0):.2g}' == '0'
+    assert f'{mp.mpf(0):.3g}' == '0'
+    assert f'{mp.mpf(0):.4g}' == '0'
+    assert f'{mp.mpf(0):.10g}' == '0'
+    assert f'{mp.mpf(0):.50g}' == '0'
+    assert f'{mp.mpf(0):.100g}' == '0'
+
+    # precision 0 doesn't make a lot of sense for the 'g' code (what does
+    # it mean to have no significant digits?); in practice, it's interpreted
+    # as identical to precision 1
+    assert f'{mp.mpf(1000):.0g}' == '1e+03'
+    assert f'{mp.mpf(100):.0g}' == '1e+02'
+    assert f'{mp.mpf(10):.0g}' == '1e+01'
+    assert f'{mp.mpf(1):.0g}' == '1'
+    assert f'{mp.mpf(0.1):.0g}' == '0.1'
+    assert f'{mp.mpf(0.01):.0g}' == '0.01'
+    assert f'{mp.mpf(1e-3):.0g}' == '0.001'
+    assert f'{mp.mpf(1e-4):.0g}' == '0.0001'
+    assert f'{mp.mpf(1e-5):.0g}' == '1e-05'
+    assert f'{mp.mpf(1e-6):.0g}' == '1e-06'
+    assert f'{mp.mpf(12):.0g}' == '1e+01'
+    assert f'{mp.mpf(120):.0g}' == '1e+02'
+    assert f'{mp.mpf(1.2):.0g}' == '1'
+    assert f'{mp.mpf(0.12):.0g}' == '0.1'
+    assert f'{mp.mpf(0.012):.0g}' == '0.01'
+    assert f'{mp.mpf(0.0012):.0g}' == '0.001'
+    assert f'{mp.mpf(0.00012):.0g}' == '0.0001'
+    assert f'{mp.mpf(0.000012):.0g}' == '1e-05'
+    assert f'{mp.mpf(0.0000012):.0g}' == '1e-06'
+
+    # precision 1 identical to precision 0
+    assert f'{mp.mpf(1000):.1g}' == '1e+03'
+    assert f'{mp.mpf(100):.1g}' == '1e+02'
+    assert f'{mp.mpf(10):.1g}' == '1e+01'
+    assert f'{mp.mpf(1):.1g}' == '1'
+    assert f'{mp.mpf(0.1):.1g}' == '0.1'
+    assert f'{mp.mpf(0.01):.1g}' == '0.01'
+    assert f'{mp.mpf(1e-3):.1g}' == '0.001'
+    assert f'{mp.mpf(1e-4):.1g}' == '0.0001'
+    assert f'{mp.mpf(1e-5):.1g}' == '1e-05'
+    assert f'{mp.mpf(1e-6):.1g}' == '1e-06'
+    assert f'{mp.mpf(12):.1g}' == '1e+01'
+    assert f'{mp.mpf(120):.1g}' == '1e+02'
+    assert f'{mp.mpf(1.2):.1g}' == '1'
+    assert f'{mp.mpf(0.12):.1g}' == '0.1'
+    assert f'{mp.mpf(0.012):.1g}' == '0.01'
+    assert f'{mp.mpf(0.0012):.1g}' == '0.001'
+    assert f'{mp.mpf(0.00012):.1g}' == '0.0001'
+    assert f'{mp.mpf(0.000012):.1g}' == '1e-05'
+    assert f'{mp.mpf(0.0000012):.1g}' == '1e-06'
+
+    # precision 2
+    assert f'{mp.mpf(1000):.2g}' == '1e+03'
+    assert f'{mp.mpf(100):.2g}' == '1e+02'
+    assert f'{mp.mpf(10):.2g}' == '10'
+    assert f'{mp.mpf(1):.2g}' == '1'
+    assert f'{mp.mpf(0.1):.2g}' == '0.1'
+    assert f'{mp.mpf(0.01):.2g}' == '0.01'
+    assert f'{mp.mpf(0.001):.2g}' == '0.001'
+    assert f'{mp.mpf(1e-4):.2g}' == '0.0001'
+    assert f'{mp.mpf(1e-5):.2g}' == '1e-05'
+    assert f'{mp.mpf(1e-6):.2g}' == '1e-06'
+    assert f'{mp.mpf(1234):.2g}' == '1.2e+03'
+    assert f'{mp.mpf(123):.2g}' == '1.2e+02'
+    assert f'{mp.mpf(12.3):.2g}' == '12'
+    assert f'{mp.mpf(1.23):.2g}' == '1.2'
+    assert f'{mp.mpf(0.123):.2g}' == '0.12'
+    assert f'{mp.mpf(0.0123):.2g}' == '0.012'
+    assert f'{mp.mpf(0.00123):.2g}' == '0.0012'
+    assert f'{mp.mpf(0.000123):.2g}' == '0.00012'
+    assert f'{mp.mpf(0.0000123):.2g}' == '1.2e-05'
+
+    # bad cases from http://bugs.python.org/issue9980
+    assert f'{mp.mpf(38210.0):.12g}' == '38210'
+    assert f'{mp.mpf(37210.0):.12g}' == '37210'
+    assert f'{mp.mpf(36210.0):.12g}' == '36210'
+
+    # alternate g formatting:  always include decimal point and
+    # exactly <precision> significant digits.
+    assert f'{mp.mpf(0):#.0g}' == '0.'
+    assert f'{mp.mpf(0):#.1g}' == '0.'
+    assert f'{mp.mpf(0):#.2g}' == '0.0'
+    assert f'{mp.mpf(0):#.3g}' == '0.00'
+    assert f'{mp.mpf(0):#.4g}' == '0.000'
+
+    assert f'{mp.mpf(0.2):#.0g}' == '0.2'
+    assert f'{mp.mpf(0.2):#.1g}' == '0.2'
+    assert f'{mp.mpf(0.2):#.2g}' == '0.20'
+    assert f'{mp.mpf(0.2):#.3g}' == '0.200'
+    assert f'{mp.mpf(0.2):#.4g}' == '0.2000'
+    assert f'{mp.mpf(0.2):#.10g}' == '0.2000000000'
+
+    assert f'{mp.mpf(2):#.0g}' == '2.'
+    assert f'{mp.mpf(2):#.1g}' == '2.'
+    assert f'{mp.mpf(2):#.2g}' == '2.0'
+    assert f'{mp.mpf(2):#.3g}' == '2.00'
+    assert f'{mp.mpf(2):#.4g}' == '2.000'
+
+    assert f'{mp.mpf(20):#.0g}' == '2.e+01'
+    assert f'{mp.mpf(20):#.1g}' == '2.e+01'
+    assert f'{mp.mpf(20):#.2g}' == '20.'
+    assert f'{mp.mpf(20):#.3g}' == '20.0'
+    assert f'{mp.mpf(20):#.4g}' == '20.00'
+
+    assert f'{mp.mpf(234.56):#.0g}' == '2.e+02'
+    assert f'{mp.mpf(234.56):#.1g}' == '2.e+02'
+    assert f'{mp.mpf(234.56):#.2g}' == '2.3e+02'
+    assert f'{mp.mpf(234.56):#.3g}' == '235.'
+    assert f'{mp.mpf(234.56):#.4g}' == '234.6'
+    assert f'{mp.mpf(234.56):#.5g}' == '234.56'
+    assert f'{mp.mpf(234.56):#.6g}' == '234.560'
+
+
+def test_mpf_float():
+    '''
+    These are additional random tests that check that mp.mpf and fp.mpf yield
+    the same results for default precision.
+    '''
+
+    for _ in range(10000):
+        fmt_str = random_fmt()
+        num = random.choice(
+                (
+                    random.uniform(-1e300, 1e300),
+                    random.uniform(-1e-300, 1e-300)
+                    )
+                )
+
+        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
+
+    # These additional tests test for mpf initialized form subnormal floats
+    for _ in range(10000):
+        fmt_str = random_fmt()
+        num = fp.mpf('{:.15f}e{:d}'.format(
+                     random.uniform(1, 10), random.randint(-324, -308)))
+
+        # We skip the case when num == 0 since mpmath does not have negative
+        # zero.
+        if num != 0 and random.randint(0, 1):
+            num = -num
+        assert fmt_str.format(num) == fmt_str.format(mp.mpf(num))
+
+
+def test_mpf_fmt():
+    '''
+    These tests are either specific tests to mpf, or tests that cover
+    code that is not covered in the CPython tests.
+    '''
+
+    with workdps(1000):
+        # Numbers with more than 15 significant digits
+        # fixed format
+        assert f"{mp.mpf('1.234567890123456789'):.20f}" == '1.23456789012345678900'
+        assert f"{mp.mpf('1.234567890123456789'):.25f}" == '1.2345678901234567890000000'
+        assert f"{mp.mpf('1.234567890123456789'):.30f}" == '1.234567890123456789000000000000'
+        assert f"{mp.mpf('1e-50'):.50f}" == '0.00000000000000000000000000000000000000000000000001'
+
+        # scientific notation
+        assert f"{mp.mpf('1.234567890123456789'):.20e}" == '1.23456789012345678900e+00'
+        assert f"{mp.mpf('1.234567890123456789'):.25e}" == '1.2345678901234567890000000e+00'
+        assert f"{mp.mpf('1.234567890123456789'):.30e}" == '1.234567890123456789000000000000e+00'
+        assert f"{mp.mpf('1e-50'):.50e}" == '1.00000000000000000000000000000000000000000000000000e-50'
+
+        # width and fill char
+        assert f"{mp.mpf('0.01'):z<10.5f}" == '0.01000zzz'
+        assert f"{mp.mpf('0.01'):z^10.5f}" == 'z0.01000zz'
+        assert f"{mp.mpf('0.01'):z>10.5f}" == 'zzz0.01000'
+        assert f"{mp.mpf('0.01'):z=10.5f}" == 'zzz0.01000'
+
+        assert f"{mp.mpf('0.01'):z<+10.5f}" == '+0.01000zz'
+        assert f"{mp.mpf('0.01'):z^+10.5f}" == 'z+0.01000z'
+        assert f"{mp.mpf('0.01'):z>+10.5f}" == 'zz+0.01000'
+        assert f"{mp.mpf('0.01'):z=+10.5f}" == '+zz0.01000'
+
+        assert f"{mp.mpf('-0.01'):z<10.5f}" == '-0.01000zz'
+        assert f"{mp.mpf('-0.01'):z^10.5f}" == 'z-0.01000z'
+        assert f"{mp.mpf('-0.01'):z>10.5f}" == 'zz-0.01000'
+        assert f"{mp.mpf('-0.01'):z=10.5f}" == '-zz0.01000'
+
+        assert f"{mp.mpf('0.01'):z<15.5e}" == '1.00000e-02zzzz'
+        assert f"{mp.mpf('0.01'):z^15.5e}" == 'zz1.00000e-02zz'
+        assert f"{mp.mpf('0.01'):z>15.5e}" == 'zzzz1.00000e-02'
+        assert f"{mp.mpf('0.01'):z=15.5e}" == 'zzzz1.00000e-02'
+
+        assert f"{mp.mpf('0.01'):z<+15.5e}" == '+1.00000e-02zzz'
+        assert f"{mp.mpf('0.01'):z^+15.5e}" == 'z+1.00000e-02zz'
+        assert f"{mp.mpf('0.01'):z>+15.5e}" == 'zzz+1.00000e-02'
+        assert f"{mp.mpf('0.01'):z=+15.5e}" == '+zzz1.00000e-02'
+
+        assert f"{mp.mpf('-0.01'):z<15.5e}" == '-1.00000e-02zzz'
+        assert f"{mp.mpf('-0.01'):z^15.5e}" == 'z-1.00000e-02zz'
+        assert f"{mp.mpf('-0.01'):z>15.5e}" == 'zzz-1.00000e-02'
+        assert f"{mp.mpf('-0.01'):z=15.5e}" == '-zzz1.00000e-02'
+
+        # capitalized scientific notation
+        assert f"{mp.mpf('-0.01'):z<15.5E}" == '-1.00000E-02zzz'
+
+        # generalized format
+        assert f"{mp.mpf('1.234567890123456789'):.20g}" == '1.234567890123456789'
+        assert f"{mp.mpf('1.234567890123456789'):.25g}" == '1.234567890123456789'
+        assert f"{mp.mpf('1.234567890123456789'):.30g}" == '1.234567890123456789'
+        assert f"{mp.mpf('1e-50'):.50g}" == '1e-50'
+        assert f"{mp.mpf('1e-50'):.50G}" == '1E-50'
+
+        # thousands separator
+        assert f"{mp.mpf('1e9'):,.0f}" == '1,000,000,000'
+        assert f"{mp.mpf('123456789.0123456'):,.4f}" == '123,456,789.0123'
+        assert f"{mp.mpf('1234567890.123456'):_.4f}" == '1_234_567_890.1235'
+        assert f"{mp.mpf('1234.5678'):_.4f}" == '1_234.5678'
+
+        assert f"{mp.mpf('1e9'):,.0e}" == '1e+09'
+        assert f"{mp.mpf('123456789.0123456'):,.4e}" == '1.2346e+08'
+        assert f"{mp.mpf('1234567890.123456'):_.4e}" == '1.2346e+09'
+        assert f"{mp.mpf('1234.5678'):_.4e}" == '1.2346e+03'
+
+        # Tests for no_neg_0
+        assert f"{mp.mpf('-1e-4'):,.2f}" == '-0.00'
+        assert f"{mp.mpf('-1e-4'):z,.2f}" == '0.00'
+
+        # Tests for = alignment
+        assert f"{mp.mpf('0.24'):=+20.2f}" == '+               0.24'
+        assert f"{mp.mpf('0.24'):=+020.2e}" == '+000000000002.40e-01'
+        assert f"{mp.mpf('0.24'):=+020.2g}" == '+0000000000000000.24'
+
+
+def test_mpf_fmt_special():
+    assert f'{inf:f}' == 'inf'
+    assert f'{inf:+f}' == '+inf'
+    assert f'{inf:F}' == 'INF'
+    assert f'{inf:+F}' == '+INF'
+
+    assert f'{ninf:f}' == '-inf'
+    assert f'{ninf:+f}' == '-inf'
+    assert f'{ninf:F}' == '-INF'
+    assert f'{ninf:+F}' == '-INF'
+
+    assert f'{nan:f}' == 'nan'
+    assert f'{nan:+f}' == '+nan'
+    assert f'{nan:F}' == 'NAN'
+    assert f'{nan:+F}' == '+NAN'
+
+    assert f'{inf:e}' == 'inf'
+    assert f'{inf:+e}' == '+inf'
+    assert f'{inf:E}' == 'INF'
+    assert f'{inf:+E}' == '+INF'
+
+    assert f'{ninf:e}' == '-inf'
+    assert f'{ninf:+e}' == '-inf'
+    assert f'{ninf:E}' == '-INF'
+    assert f'{ninf:+E}' == '-INF'
+
+    assert f'{nan:e}' == 'nan'
+    assert f'{nan:+e}' == '+nan'
+    assert f'{nan:E}' == 'NAN'
+    assert '{:+E}'.format(nan) == '+NAN'
+
+    assert f'{inf:g}' == 'inf'
+    assert f'{inf:+g}' == '+inf'
+    assert f'{inf:G}' == 'INF'
+    assert f'{inf:+G}' == '+INF'
+
+    assert f'{ninf:g}' == '-inf'
+    assert f'{ninf:+g}' == '-inf'
+    assert f'{ninf:G}' == '-INF'
+    assert f'{ninf:+G}' == '-INF'
+
+    assert f'{nan:g}' == 'nan'
+    assert f'{nan:+g}' == '+nan'
+    assert f'{nan:G}' == 'NAN'
+    assert f'{nan:+G}' == '+NAN'
+
+    # Test the same random formats with special numbers
+    for num in (float('inf'), -float('inf'), float('nan'), 0):
+        fmt_str = random_fmt()
+        assert fmt_str.format(fp.mpf(num)) == fmt_str.format(mp.mpf(num))
+
+
+def test_errors():
+    with pytest.raises(ValueError):
+        # wrong format type
+        f"{mp.mpf('-4'):22.15k}"
+
+    with pytest.raises(ValueError, match="Invalid format specifier '<z15.e'"):
+        # no precision specified after .
+        f"{mp.mpf('-0.01'):<z15.e}"
+
+    with pytest.raises(ValueError, match="Invalid format specifier '10.5fk'"):
+        f"{mp.mpf('4'):10.5fk}"
+
+    with pytest.raises(ValueError, match="Invalid format specifier '12.3 E '"):
+        f"{mp.mpf('4'):12.3 E }"
+
+    with pytest.raises(ValueError, match="Cannot specify both 0-padding "
+                       "and a fill character"):
+        f"{mp.mpf('4'):q<03f}"