Managing numerical precision is a cornerstone of double-entry accounting. In digital bookkeeping, especially when dealing with multiple currencies, stock prices, and fractional shares, small rounding discrepancies can quickly lead to frustrating balancing errors. Beancount provides a sophisticated but intuitive system for handling precision and setting acceptable tolerances. This guide will walk you through how it works. ⚙️
Every number on this page was checked against Beancount 3.2.3, including the boundaries: each example states which residual is accepted and which one is one digit too far.
Core Precision Concepts
Beancount's primary goal is to ensure that every transaction balances to zero. However, calculations involving prices or costs often produce results with more decimal places than are practical to record. The tolerance system allows for small, acceptable imbalances.
Automatic Tolerance Inference
By default, Beancount infers the required tolerance for each transaction automatically. This inference is handled individually for every transaction and calculated separately for each currency involved.
The rule is one multiplication: the tolerance for a currency is the smallest digit seen in that currency's posting amounts, times the tolerance_multiplier option, which defaults to 0.5. At that default the tolerance is half of the last significant digit.
For example, consider this purchase:
2013-04-03 * "Buy Fund"
Assets:Fund 10.22626 FUND {37.61 USD}
Assets:Cash -384.61 USDBeancount infers the tolerances as follows:
- For the
FUNDcommodity, the number10.22626has 5 decimal places. The tolerance is half of the last digit, so $0.00001 \div 2 = 0.000005$FUND. - For the
USDcommodity, the number-384.61has 2 decimal places. The tolerance is half of the last digit, so $0.01 \div 2 = 0.005$USD.
The cash leg is what the tolerance is measured against: 10.22626 × 37.61 is 384.6096386, so this transaction is 0.0003614 USD short of zero and loads. Round the cash leg to -384.60 and the gap becomes 0.0096386 USD, past the 0.005 tolerance, and Beancount reports Transaction does not balance.
Transaction Weight Rules
When checking if a transaction balances, Beancount calculates the "weight" of each posting. The rules for this calculation are:
- Simple Amount: If a posting has only an amount (e.g.,
Assets:Cash -100.00 USD), its weight is that exact amount. - Price Posting: If a posting has a per-unit price (e.g.,
10 FUND @ 38.46 USD), its weight is theamount × price. - Per-unit Cost: Single braces hold the cost of one unit, so
10 FUND {384.61 USD}weighs10 × 384.61 = 3,846.10 USD, not384.61 USD. - Total Cost: Double braces hold the cost of the whole posting, so
10 FUND {{384.61 USD}}weighs384.61 USD. Beancount converts it to a per-unit cost of38.461 USDwhen it stores the lot. - Cost and Price: If a posting has both a cost and a per-unit price (e.g.,
10 FUND {384.61 USD} @ 400.00 USD), only the cost is used for balancing. The price is recorded for reporting, not for arithmetic.
Rules 3 and 4 are the ones that cost people an afternoon, so here they are side by side in a file that loads:
1970-01-01 open Assets:Fund
1970-01-01 open Assets:Cash
; Per-unit cost: ten units at 384.61 each, so 3,846.10 USD leaves the
; cash account.
2013-04-03 * "Broker" "Buy at a per-unit cost"
Assets:Fund 10 FUND {384.61 USD}
Assets:Cash -3846.10 USD
; Total cost: the braces double and 384.61 USD is the entire purchase.
; The lot is stored at 38.461 USD per unit.
2013-04-04 * "Broker" "Buy at a total cost"
Assets:Fund 10 FUND {{384.61 USD}}
Assets:Cash -384.61 USDThe account ends with 20 FUND in two lots, 4,230.71 USD of cost basis between them.
Precision Inference Rules
The automatic inference system follows a few specific rules:
- Number Format
- Integer amounts (e.g.,
10 USD) do not contribute to precision inference. - One decimal place is the coarsest an amount can imply:
0.1 × 0.5 = 0.05units. Beyond that you needtolerance_multiplieror a per-currency default, both below. - Costs and prices (e.g.,
{37.61 USD}) are excluded from tolerance inference by default. Only the primary amounts of the postings are used. - If postings for the same currency have different precisions (e.g.,
-10.10 USDand5.123 USD), Beancount uses the coarsest (largest) tolerance. In this case, it would be based on-10.10 USD, yielding a tolerance of $0.005$USD.
-
Default Handling You can set a global or currency-specific default tolerance if a transaction has no numbers with decimal places from which to infer it.
; Sets a default tolerance for all currencies without explicit rules option "inferred_tolerance_default" "*:0.001" ; Sets a specific default tolerance for USD option "inferred_tolerance_default" "USD:0.003" -
Tolerance Multiplier The option is
tolerance_multiplier, and it is the fraction of the smallest digit that counts as tolerable — not a percentage added on top. Its default is0.5, so setting1.2does not loosen checks by 20%: it makes every inferred tolerance 2.4 times the default one.option "tolerance_multiplier" "1.2" 1970-01-01 open Assets:Cash 1970-01-01 open Expenses:Fees ; The coarsest amount has two decimals, so the tolerance is ; 1.2 x 0.01 = 0.012 USD, and this residual of exactly 0.012 passes. ; At the default 0.5 the tolerance would be 0.005 and this would fail. 2024-05-01 * "Bank" "Wire fee" Expenses:Fees 100.00 USD Assets:Cash -99.988 USDThe older name
inferred_tolerance_multipliersets the same value but reportsRenamed to 'tolerance_multiplier'.as a load error. -
Cost-Based Inference While costs are normally ignored for tolerance inference, you can instruct Beancount to use them. This is helpful when the final amount (e.g., a cash withdrawal) is the most precise number in a transaction.
option "infer_tolerance_from_cost" "TRUE"
Here is the plain default, with no options at all, at its exact boundary:
1970-01-01 open Assets:Cash
1970-01-01 open Expenses:Fees
; Two decimals on the coarsest amount, so the tolerance is
; 0.5 x 0.01 = 0.005 USD. This residual is exactly 0.005 and passes;
; -99.994 would be 0.006 and would fail.
2024-05-01 * "Bank" "Wire fee"
Expenses:Fees 100.00 USD
Assets:Cash -99.995 USDBalance Assertions
Balance assertions (balance) are used to verify that your account's balance matches a known value on a specific date. They also have an associated tolerance.
Basic Format
The tolerance for a balance assertion is inferred from the number of decimal places in the amount, but it is twice as generous as the one used inside a transaction: tolerance_multiplier × 2 × the smallest digit. At the default multiplier that is exactly one unit of the last decimal place you wrote.
; Asserts the balance is 4.271 RGAGX with a tolerance of +/-0.001
2015-05-08 balance Assets:Fund 4.271 RGAGX
; Asserts the balance is 4.27 RGAGX with a tolerance of +/-0.01
2015-05-08 balance Assets:Fund 4.27 RGAGXThe comparison is inclusive: a difference exactly equal to the tolerance still passes. For the second example, any balance from $4.26$ to $4.28$ passes the check, and 4.2801 fails with Balance failed for 'Assets:Fund': expected 4.27 RGAGX != accumulated 4.2801 RGAGX (0.0101 too much).
1970-01-01 open Assets:Fund
1970-01-01 open Equity:Opening-Balances
1970-01-02 * "Broker" "Opening position"
Assets:Fund 4.28 RGAGX
Equity:Opening-Balances -4.28 RGAGX
; 4.28 is 0.01 away from the asserted 4.27, which is the whole tolerance.
2015-05-08 balance Assets:Fund 4.27 RGAGXExplicit Tolerances
If the inferred tolerance is not suitable, you can specify one explicitly using the tilde (~) character. This is the only explicit-tolerance syntax Beancount has, and it works on balance directives only — a tilde inside a transaction posting is a syntax error.
1970-01-01 open Assets:Fund
1970-01-01 open Equity:Opening-Balances
1970-01-02 * "Broker" "Opening position"
Assets:Fund 4.281 RGAGX
Equity:Opening-Balances -4.281 RGAGX
; Asserts the balance is 4.271 RGAGX with a custom tolerance of
; +/-0.01 RGAGX, so anything from 4.261 to 4.281 passes.
2015-05-08 balance Assets:Fund 4.271 ~ 0.01 RGAGXPush the holding to 4.2811 and the same assertion fails by 0.0101.
Rounding Management
Small residuals from cost and price arithmetic are normal. What Beancount does with them is narrower than it looks.
Rounding Error Tracking
The account_rounding option names an account meant to absorb residuals. It takes a complete account name and is stored exactly as you write it — no equity prefix is added, unlike the equity account options.
option "account_rounding" "Equity:Rounding"
1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
; 1.245 x 43.23 = 53.82135, so this is 0.00135 USD short of balancing.
2013-02-23 * "Broker" "Purchase"
Assets:Invest 1.245 RGAGX {43.23 USD}
Assets:Cash -53.82 USDIn this transaction, . The transaction is imbalanced by $-0.00135$ USD, which is inside the inferred 0.005 USD tolerance, so it loads.
On Beancount 3.2.3 nothing is posted to Equity:Rounding. The option parses and is stored, but no stage of the loader inserts the residual posting, so the account ends at zero and a residual that is outside tolerance is still an error rather than being swept up:
option "account_rounding" "Equity:Rounding"
1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
; 0.10135 USD out, far past the 0.005 tolerance. Setting
; account_rounding does not rescue it:
; Transaction does not balance: (0.10135 USD)
2013-02-23 * "Broker" "Purchase"
Assets:Invest 1.245 RGAGX {43.23 USD}
Assets:Cash -53.72 USDSo treat account_rounding as inert on this version. If you want a residual recorded rather than tolerated, write the third posting yourself:
1970-01-01 open Assets:Invest
1970-01-01 open Assets:Cash
1970-01-01 open Equity:Rounding
2013-02-23 * "Broker" "Purchase"
Assets:Invest 1.245 RGAGX {43.23 USD}
Assets:Cash -53.82 USD
Equity:Rounding -0.00135 USDThat version balances to exactly zero, and the dust is visible in an account you can report on.
Inferred Number Precision
Beancount does not round the numbers you write. There is no default_tolerance option — it does not exist and fails the load with Invalid option: 'default_tolerance' — and no setting quantizes stored amounts.
-
Storage is always exact. Write
53.82135 USDand the ledger holds53.82135 USD, whatever your tolerance settings say. Tolerance decides whether a transaction is accepted; it never edits a number. -
Display is a separate setting.
display_precisionfixes how many fractional digits a currency is rendered with, and changes nothing about the stored value or the balance check.option "display_precision" "USD:0.01" 1970-01-01 open Assets:Cash 1970-01-01 open Income:Interest ; Rendered as 53.82 USD, stored as 53.82135 USD. 2024-06-30 * "Bank" "Interest" Assets:Cash 53.82135 USD Income:Interest -53.82135 USD -
Rounding is your posting to write. If you want the residual out of the arithmetic, round the amount in the source and book the difference explicitly, as in the three-posting example above.
Implementation Details
A few technical points clarify how Beancount achieves this reliability.
-
Number Representation: Beancount uses Python's
decimalmodule, not floating-point numbers. The default context carries 28 significant digits — total digits, not digits after the point — which avoids the binary representation errors common to floats. -
DisplayContext Class: This internal class handles all number formatting for display purposes. It infers each currency's precision from the numbers in your file unless
display_precisionpins it, and can format output with aligned columns and commas. -
Precision vs. Tolerance: It's crucial to distinguish these two concepts:
- Precision relates to the display format of a number (how many decimal places are shown).
- Tolerance is the allowance for imbalance used during verification checks.
Best Practices ✨
Here are some practical recommendations for managing precision in your ledger.
Initial Setup
For most new ledgers, this is a robust starting configuration:
; A floor for currencies that have no decimals to infer from
option "inferred_tolerance_default" "*:0.005"
; Leave the multiplier at its 0.5 default unless a real institution
; forces your hand; 1.2 would mean 2.4x the usual tolerance.
option "tolerance_multiplier" "0.5"Troubleshooting Tips
If you encounter balancing errors:
- Add decimal digits to a posting's amount to create a tighter, more accurate local tolerance inference.
- Use explicit tolerances (
~) onbalanceassertions that fail due to predictable discrepancies. - Book the residual to a dedicated account with a real third posting, so you can report on how often it happens.
- Consider setting currency-specific defaults if you frequently deal with currencies that have different conventions (e.g., JPY has no decimals).
Migration Strategy
When applying these concepts to an existing, messy ledger:
- Start with a generous global tolerance (e.g.,
*:0.05) and a highertolerance_multiplierto get the file to validate. - Gradually tighten the tolerances and fix the errors that appear.
- Add explicit digits to amounts in problematic transactions to let inference do its job.
- Monitor the rounding account's balance. A large or rapidly growing balance may signal a systemic issue that needs investigation.