Format strings
An f-string is a string literal with the prefix f: f"...", f'...',
f"""...""", or f'''...'''. Literal text uses the ordinary
string escapes, including \u{H...}; {{ and }} are
literal braces, and a single } is an error. Each {...} is a replacement
field:
{field[=][!conversion][:spec]}
The expression f"..." has type Result[str, AllocError], like a collection
literal: s = f"x={x}"? or print(f"x={x}"?)?. Its type is
Result[str, AllocError | FormatError] when formatting can fail other than by
allocation: a field runs a user display or debug method,
directly or for an element of an aggregate, a spec has
nested fields, or a spec has type c. The choice
is made per site from the fields’ types; in a generic function it is made for
each instantiation. out.format(f"...") writes one
to an existing formatter instead. The
compiler parses the template and checks every spec while compiling. It lowers
the literal to calls on a std.fmt string formatter,
with no format-string interpreter at run time. A module that uses an f-string
loads std.fmt without an import. FormatError is a
prelude name, so a signature names it without an
import too (issue #632).
from std.fmt import Formatter
class Tag:
name: str
def display[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
out.format(f"#{self.name}")
def plain(n: i64) -> Result[str, AllocError]:
pair = [n, n]?
Ok(f"n={n:>4} {pair}"?)
def tagged(tag: &Tag, width: i64) -> Result[str, AllocError | FormatError]:
Ok(f"{tag} {1:{width}}"?)
def main() -> Result[(), AllocError | IoError | FormatError]:
print(plain(7)?)?
print(tagged(&Tag("x"), 3)?)?
Ok(())
n= 7 [7, 7]
#x 1
A function that declares only FormatError for an f-string with built-in
spellings is rejected, and the diagnostic names AllocError as the
replacement.
Fields
A field is any expression, for example {p.x}, {xs[i + 1]},
{total / count:.2f}, {a > b}, {'yes' if ok else 'no'}, or
{parse(text)?}. As in Python, {a, b} is the tuple (a, b). A : or !
outside brackets ends the expression, so a lambda must be parenthesized.
Errors inside a field are reported at their position in the source.
All fields, including the nested fields of specs, are evaluated once, left to
right, before any text is built. Each value is lent to the formatting code, so
formatting moves nothing; a temporary value lives until the f-string’s text is
built. A ? in a field belongs to the enclosing function: on Err or
Nothing it returns from that function before formatting starts, exactly as
? in a call argument does. The Result of the f-string itself reports only
formatting failure, so f"{parse(s)?}"? has two independent ?s.
A field cannot contain a backslash, a comment, or the quote character that encloses the f-string. A field in a triple-quoted f-string may span lines; a line break in its spec is a character of the spec, such as a fill.
Only the f prefix starts an f-string. F"..." and the combinations bf/fb
are errors: build text with an f-string, then convert it to bytes.
{expr=} writes the field’s source text, including the = and any spaces
around it, then its value. Without a conversion or a spec the value is written
as with !r, so {name=} gives name="Ada".
Quantity fields apply numeric specs to
the stored number and append the coherent-unit symbol. Use !s to pad the
whole display text, or !r for Quantity[T, D](value) debug output.
Values and conversions
Without a conversion, a field is written as print writes it, including its
user formatting methods:
| Type | Spelling |
|---|---|
str | The text itself |
bool | True or False |
| Integers | Decimal |
f32, f64 | Shortest round-trip, as str.from |
A class or enum with a display method | That method, with the depth accounting of std.fmt.display |
A class or enum with only a debug method | That method |
Anything else, including bytes | The str.repr spelling, as print shows aggregates and b"..." values; elements with a debug method use it |
A field calls a top-level display or debug from the module where the
f-string is written, as a direct call there would. A private class, or a
private method, formats in its own module. In another module, including through
a generic function defined there, the class and the method must be public;
otherwise the field is rejected with the ordinary private-access diagnostic.
Methods of aggregate elements run as str.repr runs them, with their own
module’s access.
!s is the same spelling. !r writes str quoted and escaped as
Formatter.debug_text does, uses a type’s debug method when it has
one, and otherwise the str.repr spelling, including element debug methods. !a, like Python’s ascii(),
takes the !r text and spells each non-ASCII scalar as \u{hex} with
lowercase digits, the escape debug_text uses for controls:
f"{'café'!a}" is "caf\u{e9}", which is also a valid string literal. Python spells the same escapes \xe9,
\u2603, or \U0001f600, with single quotes.
A @redacted type without its own
display or debug method writes TypeName(@redacted) under every conversion and
spec, including !r, !a, and =.
Format specs
A spec follows Python’s format specification mini-language:
[[fill]align][sign][z][#][0][width][grouping][.precision][type]
alignis<(left),>(right),^(center; extra fill goes right), or=(fill between the sign and the digits, numbers only). Text defaults to left and numbers to right.fillis any single character.signis-(default),+, or a space.z(floats only) drops the sign of a result that rounds to zero, so{-0.0001:z.2f}is0.00;-infkeeps it.#adds0b,0o,0x, or0Xto integers, and keeps the point or trailing zeros of floats.0pads numbers with zeros after the sign and prefix. With grouping, the zeros are grouped too:{-1234:010,}gives-0,001,234. After an explicit alignment,0is the fill character.widthis a minimum count of Unicode scalars; output is never truncated to it.groupingis,or_: every three integer digits, or every four with_and typesx,X,o, andb.precisionis the digits after the point fore,f, and%, the significant digits forgand for a float with no type, and the maximum number of scalars for text.
Width and precision are decimal numbers of at most 4096.
| Value | Types | Other limits |
|---|---|---|
Text (str, bool, values with a conversion or no built-in spelling) | none, s | No sign, z, #, 0, grouping, or = |
| Integers | none, d, x, X, o, b, n, c | No precision or z; , only for decimal; c takes no sign, #, or grouping |
f32, f64 | none, e, E, f, F, g, G, %, n |
c writes the Unicode scalar whose code point is the integer, laid out like a
number: right-aligned by default, and 0 pads with zeros. A value that is not
a scalar (negative, a surrogate, or above U+10FFFF) makes the f-string return
Err(FormatError.InvalidSpec), so a field with c adds FormatError to the
f-string’s type. n is Python’s n in the C locale, which is
the locale Python formats with unless a program calls locale.setlocale: d
for integers and g for floats, with no grouping. Plenty has no locales, so
grouping with n is rejected rather than ignored.
Float types follow Python, including exponents with a sign and at least two
digits (1.23e+03), % multiplying by 100, and g choosing fixed or exponent
notation. Typed floats spell infinity and NaN as inf and nan (INF, NAN
for upper-case types). A float without a type or precision keeps the print
spelling (1e16, NaN); with a precision it behaves as Python’s .Ng
with at least one digit after the point. Because Plenty has a bool type, a
spec on a bool formats its text rather than the number 1 or 0.
An unsupported option for the field’s type is a compile error at the spec,
for example format spec `.2f` does not apply to i64: integers take no precision; convert with f64(...) to format a float. Convert explicitly to use
another type’s options.
Nested fields in a spec
A spec may contain fields, as in {x:{width}} or
{x:{fill}{align}{width}.{precision}f}. A nested field is an expression with
no =, conversion, spec, or further nesting. Python pastes nested values into
the spec text and parses the result at run time; Plenty instead parses the
spec while compiling, and each nested field supplies exactly one option,
chosen by its position and its type:
| Nested field | Type | Supplies |
|---|---|---|
First, before an alignment character or a second str field | str | The fill |
| First otherwise, or second after the fill | str | The alignment |
| Where the width’s digits would be | Any integer | The width |
After . | Any integer | The precision |
Any other nested field, such as an integer before the alignment or two adjacent
integer fields, is a compile error, as is 0 after a nested alignment (write
the fill instead). Everything written in the spec, including its type, is
checked against the field’s type as usual; {x:{w}.2f} with an integer x is
rejected because of .2f whatever w is.
The nested values are checked when the f-string runs, and an invalid one makes
it return Err(FormatError.InvalidSpec) before any text is built:
- an alignment must be
"",<,>,^, or=(=only for numbers);""means the default alignment; - a fill must be one scalar, or
""for the default space, and a non-empty fill needs a non-empty alignment; - a width or precision must be in
0..=4096.
For valid values the result is the one Python gives. Where Python would read a
pasted value differently, such as a str field holding "<10" or a fill with
an empty alignment, Plenty returns InvalidSpec rather than another layout.
Writing to a formatter
out.format(f"..."), where out is a std.fmt.Formatter[W]
or a &mut reference to one, writes the f-string to out without building a
str, and has type Result[(), FormatError]. It is the form to use in a
display or debug method:
pub def display[W](self, out: &mut Formatter[W]) -> Result[(), FormatError]:
out.format(f"({self.x}, {self.y})")
The argument must be an f-string literal; the compiler lowers the pair to
calls on out, so out’s byte budget, depth limit, and partial-progress rules
apply, and its scratch is reused. A field that needs a temporary string (a
spec on a value without a built-in spelling, !r on str with a spec, !a)
still allocates one. format is not an ordinary method of Formatter: it has
no other form, and a user type’s own format method is unaffected.
Cost and failure
The result of f"..." is built in a new string formatter whose output and
32-byte scratch are allocated for each evaluation; a float spelling longer than the scratch
grows it. A spec on a value without a built-in spelling first builds that
value’s text as a temporary string. There is no byte budget: an f-string is
bounded only by allocation failure, reported as AllocError whatever the
f-string’s type; a user method’s own FormatError.Allocation(...)
is reported as AllocError too. User display and debug
methods may nest through std.fmt up to 32 levels, after which the result is
FormatError.RecursionLimit. No partial string is returned on error.