Formatting Values

tinytab has three functions for turning a single number into a display string. They share the same rules: the value is the first argument, and every option is a keyword argument.

Numbers

fmt_number() rounds to a fixed number of decimal places (2 by default) and groups thousands with a separator:

from tinytab import fmt_number

fmt_number(1234567.891)
'1,234,567.89'

Use decimals= to change the precision:

fmt_number(1234567.891, decimals=0)
'1,234,568'

The thousands separator is a comma by default. Set another with sep_mark=:

fmt_number(1234567.891, sep_mark=" ")
'1 234 567.89'

Percentages

fmt_percent() takes a proportion, where 1 means 100%, and shows one decimal place by default:

from tinytab import fmt_percent

fmt_percent(0.256)
'25.6%'
fmt_percent(0.256, decimals=0)
'26%'

Currencies

fmt_currency() adds a currency symbol and uses the right number of decimal places for the currency. US dollars are the default:

from tinytab import fmt_currency

fmt_currency(1234.5)
'$1,234.50'

Choose another currency with a three-letter code (in any case). Yen are shown without decimal places, and negative amounts put the sign before the symbol:

[
    fmt_currency(1234.5, currency="eur"),
    fmt_currency(1234.5, currency="JPY"),
    fmt_currency(-5, currency="GBP"),
]
['€1,234.50', '¥1,234', '-£5.00']

The supported codes are USD, EUR, GBP, and JPY. Any other code is an error, and the message lists the valid ones:

fmt_currency(10, currency="XYZ")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 1
----> 1 fmt_currency(10, currency="XYZ")

File ~/py_projects/example-python-package/tinytab/_formats.py:136, in fmt_currency(x, currency)
    132 if code not in CURRENCY_SYMBOLS:
    133     # List the valid choices in the message, so the user can fix the call without having to
    134     # look anything up. (Joining a dict iterates over its keys.)
    135     valid = ", ".join(CURRENCY_SYMBOLS)
--> 136     raise ValueError(f"Unknown currency {currency!r}. Valid currencies: {valid}.")
    138 decimals = 0 if code == "JPY" else 2
    139 # Format the absolute value and put the sign in front of the symbol ("-$5.00"). Formatting
    140 # the negative number directly would put the sign after it ("$-5.00").

ValueError: Unknown currency 'XYZ'. Valid currencies: USD, EUR, GBP, JPY.

Only numbers

All three functions accept only numbers (integers and floats). Anything else, including a number written as a string, raises a TypeError:

fmt_number("10")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 fmt_number("10")

File ~/py_projects/example-python-package/tinytab/_formats.py:68, in fmt_number(x, decimals, sep_mark)
     44 def fmt_number(x: float, *, decimals: int = 2, sep_mark: str = ",") -> str:
     45     """Format a number with a fixed number of decimal places and a thousands separator.
     46 
     47     Parameters
   (...)     66     '1,235'
     67     """
---> 68     _check_number(x)
     69     if decimals < 0:
     70         raise ValueError(f"`decimals` must be zero or greater, got {decimals!r}.")

File ~/py_projects/example-python-package/tinytab/_formats.py:34, in _check_number(x, arg)
     28 def _check_number(x: object, arg: str = "x") -> None:
     29     # `bool` is a subclass of `int`, but formatting `True` as "1.00" is almost always a mistake
     30     if isinstance(x, bool) or not isinstance(x, (int, float)):
     31         # The error message names the argument, shows the bad value, and says what was expected
     32         # (practices doc, item 21). `!r` formats the value with `repr()`, so a string shows up
     33         # with quotes ('10') and can't be mistaken for a number.
---> 34         raise TypeError(f"Expected a number for `{arg}`, got {type(x).__name__}: {x!r}")

TypeError: Expected a number for `x`, got str: '10'

True and False are rejected too, even though Python treats them as the integers 1 and 0. Formatting a boolean as “1.00” is almost always a mistake.