Skip to main content

beancount-zakat

CI PyPI Python versions License: MIT

Zakat calculation for Beancount ledgers, with a Fava dashboard and a standalone CLI.

Gold and silver nisab are calculated independently and presented side by side. Every figure traces back to the accounts, prices, periods and transactions that produced it.

Informational only. See the disclaimer.


Contents


What it does

  • Reads every entry loaded by your root ledger, included files and all.
  • Selects accounts purely from beancount_zakat: metadata on Open directives. No configuration file, no account list to keep in sync.
  • Reconstructs net zakatable wealth over time, re-valuing holdings in other commodities whenever their prices move.
  • Runs a layered / marginal hawl model: every distinct wealth level becomes a slice with its own independent holding period.
  • Treats the nisab as the moving threshold it is — it tracks the metal price, so it differs on almost every year of your history.
  • Computes gold and silver independently and never adds them together.
  • Reports a signed balance: positive is owed, negative is paid in excess.
  • Keeps every figure exact. No floating-point drift anywhere in the arithmetic.
  • Defaults the cutoff to today, so a quiet ledger keeps accruing hawl.

Scope

What it covers

  • Cash, bank balances and any other account you tag as an asset.
  • Holdings in other commodities — gold, silver, a foreign currency — valued from the price directives already in your ledger.
  • Stock bought for resale. Shares or inventory held as trade goods, bought with the intention of selling on, are counted at market value like any other commodity holding.
  • Debts you tag as liabilities, deducted from the total.
  • Zakat already paid, including refunds and correcting reversals.
  • Both bases. Gold and silver are calculated independently across your whole history, from the first tagged posting to the report date.

What it does not cover

  • Zakat al-fitr — the per-person charge at the end of Ramadan. Different basis, different timing.
  • Agricultural produce, livestock and rikaz. These carry their own rates and thresholds and are not modelled at all.

What is yours to decide

  • Which accounts count. Nothing is inferred from an account name — an account takes part only if you tag it. Excluding a personal-use asset means simply not tagging it.
  • Which debts are deductible. Same mechanism, same reasoning. Scholars differ sharply on mortgages and future instalments; tagging is where you express your position.
  • Whether a shareholding is trade goods or a long-term investment. The tool cannot tell them apart, and will not guess from an account name. Tag the first, leave the second alone.
  • Which basis to follow. Gold and silver are alternatives. Both are shown so you can compare them; they are never added together.
  • The nisab weights, if the authority you follow publishes different gram equivalents. They are configurable.
  • Whether this tool's method matches your position. It accrues liability in proportion to time held once the hawl is met, which is a choice and not a consensus — see How the calculation works.

Installation

Requires Python 3.10 or newer, and works with Beancount 3.x. The dashboard needs Fava 1.30 or newer.

# Calculation engine and CLI only (does not install Fava):
pip install beancount-zakat

# With the Fava dashboard:
pip install 'beancount-zakat[fava]'

Setting up your ledger

1. Tag your accounts

The beancount_zakat metadata key on an Open directive is the only thing that puts an account in scope. Three roles are recognised:

Value Meaning
"asset" Counts positively towards zakatable wealth
"liability" Counts with its natural (negative) Beancount sign
"expense" Postings here are zakat payments
2020-01-01 open Assets:Bank:Savings       PKR
  beancount_zakat: "asset"

2020-01-01 open Assets:Cash               PKR
  beancount_zakat: "asset"

2020-01-01 open Liabilities:Loan-Payable  PKR
  beancount_zakat: "liability"

2020-01-01 open Expenses:Donations:Zakat  PKR
  beancount_zakat: "expense"

;; Untagged, so ignored entirely — this is how you exclude a personal-use asset.
2020-01-01 open Assets:Vehicle            PKR

Classification is exact: tagging Assets:Bank does not pull in Assets:Bank:Savings. Tag each account you want included. Metadata works in included files just as well as in the root ledger.

Because the tags live in the ledger, your configuration travels with the data it describes and cannot drift out of step with it.

2. Record metal prices

Prices come from ordinary price directives in your own ledger. GLDTOLA and SLVTOLA are understood out of the box as prices per tola:

2026-01-01 price GLDTOLA  280000.00 PKR
2026-01-01 price SLVTOLA    3300.00 PKR

You only need a directive when the price actually moves — a day with no price of its own reuses the last known price. A price older than 90 days is still used but is flagged as stale.

To quote per gram, or to use different symbols, declare them explicitly. The unit is required, never guessed:

2020-01-01 custom "fava-extension" "beancount_zakat.fava_extension" "{
  'metal_commodities': {'XAUGRAM': ['gold', 'gram'],
                        'XAGGRAM': ['silver', 'gram']},
}"

If no price exists at or before the report date, the report says so prominently. Missing valuation data never becomes a silent zero.

3. Register the dashboard

2020-01-01 custom "fava-extension" "beancount_zakat.fava_extension" "{}"

The module path is beancount_zakat.fava_extension, not beancount_zakat.

A complete, entirely synthetic example lives in examples/ledger/main.beancount, covering assets, a liability, payments and a refund, gold and silver prices, a holding in another commodity, included files, a nisab break and recovery, and a quiet tail with no recent transactions.

The Fava dashboard

fava examples/ledger/main.beancount

Then open the Zakat report. Six tabs:

  1. Overview — side-by-side gold and silver cards showing lifetime liability, payments and the signed balance; data-quality status; accounts in scope.
  2. Yearly Summary — one row per Hijri year, reconciling exactly to the totals.
  3. Wealth & Nisab — two charts: a stacked one showing how net wealth is distributed across your accounts, then net wealth on its own against both moving thresholds. Plus the full wealth timeline, every change to either nisab with the price behind it, and account composition.
  4. Calculation Detail — gold and silver in separate, switchable sections. Each carries one chart of net wealth against that one threshold with the below-nisab stretches shaded, a hawl timeline showing every marginal slice as a band (green = complete, amber = running but short of a year, grey = reset by falling below the nisab), and a table giving the nisab range in force during each period plus a plain-language reason.
  5. Payments — signed payment detail with a running total.
  6. About Zakat & Methodology — what zakat is, how this tool models it, and where its choices are choices rather than consensus.

Notes:

  • Fava's time filter sets the report cutoff. A time filter ending 2026-10-01 gives you the position as at 2026-10-01, inclusive. The filter moves the end only; the timeline always starts at inception, because hawl has to be measured from when wealth was actually acquired. With no filter the cutoff is today.
  • One chart stacks by account, the first on Wealth & Nisab. Every tagged account is its own band: anything held stacks up from the zero line, anything owed hangs below it, so the gap between the two fronts is net zakatable wealth, with the net line drawn over the top. An overdrawn asset sits on the negative side too. Each account keeps the same colour wherever it appears. The nisab is deliberately absent here — a stack front is a gross figure, and the threshold applies to the net. Beyond eight accounts a side the smallest are pooled into a single Other band; the tables stay complete.
  • Every other chart is net wealth against a threshold — both nisabs on Wealth & Nisab, one per basis on Calculation Detail with the hawl resets shaded.
  • Charts are interactive. Switch any account or line off and what is left re-stacks and rescales; narrow the window with the date control at the top right (presets, or explicit from/to dates); hover — or tap, on a touch screen — anywhere on the plot for a crosshair readout of everything visible at that date, including the running stack total. The charts are drawn to fit the window, so they use the width the screen offers and stay legible on a phone.
  • Charts are presentation only. The bands are the same balances the calculation used, drawn as their parts; nothing on a chart feeds back into a figure.
  • Works offline and with JavaScript off. Charts are rendered by the server as images and re-drawn in the browser only to make them interactive. Nothing is fetched from a CDN.
  • It looks like Fava, because Fava styles it. The report inherits Fava's theme rather than approximating it, including the light/dark choice you make in Fava itself, even when that contradicts your operating system.
  • Tabs are keyboard-navigable, and the selected tab is kept in the URL so it can be bookmarked, linked to, and reached with the browser's back button.
  • No status is signalled by colour alone: every chip carries a word and a glyph.

Screenshots

Captured from a live Fava server running the example ledger; the full set for every tab, on desktop and mobile, in both themes, is in docs/screenshots/.

Overview Wealth & Nisab
Overview, light theme Wealth & Nisab, dark theme
Calculation Detail Payments
Calculation Detail — per-basis chart and hawl timeline Payments, dark theme

The CLI

The CLI does not require Fava.

beancount-zakat examples/ledger/main.beancount
usage: beancount-zakat [-h] [--as-of YYYY-MM-DD] [--csv PATH]
                       [--basis {both,gold,silver}] [--width N] [--quiet]
                       [--version]
                       ledger
Option Meaning
--as-of YYYY-MM-DD Report cutoff. Default today. Nothing later affects the result.
--csv PATH Also write the CSV set to a directory, or a .zip if PATH ends in .zip.
--basis Which detail tables to print.
--width N Force an output width instead of detecting the terminal.
--quiet Summary and reconciliation only.

Output degrades gracefully on narrow terminals — below about 100 columns tables become stacked label/value blocks rather than being truncated.

Examples:

# Position at a historical date, reproducibly
beancount-zakat ledger.beancount --as-of 2024-06-01

# Just the headline figures
beancount-zakat ledger.beancount --quiet

# Full report plus a CSV bundle
beancount-zakat ledger.beancount --csv ./zakat-2026.zip

Exit codes: 0 success (warnings may still be printed) · 1 a validation error makes a result untrustworthy · 2 usage error, or the ledger could not be loaded.

CSV export

--csv PATH writes seven files. Schema details in docs/csv-schema.md.

File Contents
metadata.csv Report-level facts, both bases' headline figures, assumptions
warnings.csv Every validation finding, with severity
nisab_history.csv Every change to either threshold, with the price behind it
yearly_summary.csv One row per Hijri reporting year
detail_gold.csv Gold marginal slices and holding periods
detail_silver.csv Silver marginal slices and holding periods
payments.csv Signed payment detail with a running total

Monetary columns are exact decimal strings — no grouping, no currency symbol, no locale — so a spreadsheet reads them as numbers rather than text. UTF-8, RFC 4180 line endings, stable column names and row order.

From the dashboard, the Yearly Summary, Calculation Detail and Payments tabs each offer a CSV download, plus a "Download everything (ZIP)" link.

Configuration

Account roles come from ledger metadata. Everything else is optional, has a sensible default, and is set on the fava-extension directive.

Option Default Meaning
zakat_rate 0.025 Write '2.5%' or 0.025. A bare number above 1 is rejected, not guessed.
gold_nisab_grams 87.48 Gold nisab weight
silver_nisab_grams 612.36 Silver nisab weight
nisab_gold_tola / nisab_silver_tola 7.5 / 52.5 Same thing in tola
metal_commodities GLDTOLA, SLVTOLA per tola {symbol: [metal, unit]}, unit is tola or gram
price_staleness_days 90 When to flag a carried-forward price as stale

Precedence, lowest to highest: built-in defaults → ledger Open metadata → fava-extension options and CLI flags.

Account roles are merged as a union, independently per role, so declaring one role never suppresses discovery of another. An account claimed by two roles produces a warning, and the higher-precedence source wins.

Unknown option keys, invalid roles, bad account names, negative or absurd rates, and ambiguous commodity units are all reported rather than silently ignored.

How the calculation works

The full step-by-step is on the dashboard's About Zakat & Methodology tab. In brief:

  1. Replay postings into a net-zakatable-wealth timeline. Net wealth is sum(assets) + sum(liabilities) — liabilities carry their natural negative Beancount sign, so debt is subtracted by plain addition.

  2. Take a snapshot on every posting date and every relevant price date, so holdings are re-valued when prices move.

  3. Every distinct positive wealth level becomes a marginal slice (level − previous level) with its own independent hawl.

  4. A slice's period runs while wealth stays at or above that slice's level and total wealth stays at or above the nisab. Dropping below either ends the period; recovery starts a fresh one. Each period is judged on its own, and elapsed time is never carried across a reset.

  5. Once a period reaches one full lunar year (354.36708 days):

    zakat_due = marginal_amount × elapsed_lunar_years × 2.5%
    

    This tool's position is that the hawl is the condition that makes wealth zakatable, not a restriction limiting liability to whole years; once met, liability accrues in proportion to the time held. A slice held 3.34 lunar years is charged 8.35% of that slice.

  6. Steps 3–5 run again, unchanged, against the other basis's nisab.

  7. Historical liability is computed first; payments are subtracted afterwards to give a signed balance.

This layered pro-rata model is a choice, not a scholarly consensus. Most zakat guidance has you value your wealth once a year on your zakat anniversary and pay 2.5% of it, treating the hawl as a gate that either opens or does not. This tool treats it as a gate and accrues liability in proportion to elapsed time thereafter, which suits paying accumulated zakat in Ramadan across however many years are outstanding. Discuss the method, not just the numbers, with your scholar.

The nisab is a moving threshold. It tracks the metal price, so it differs on almost every year of your history. No single figure is presented anywhere as "the" nisab: the dashboard lists every change with the price behind it, and the calculation detail shows the range in force during each period.

Two notions of "lunar year" are kept strictly apart. The calculation measures hawl only with the mean lunar year of 354.36708 days; the Umm al-Qura calendar is used only to label reporting years, so it can never move a zakat amount. Labelling covers Hijri 1343–1500 (1924-08-01 to 2077-11-16); a date outside that range is refused rather than silently approximated.

Assumptions and limitations

  • The layered pro-rata model is this tool's construction, not a consensus position — see the note above and the About tab.
  • Each slice keeps its own hawl. Newly acquired wealth begins a fresh holding period rather than being folded into an existing one.
  • No personal zakat anniversary is modelled. The tool works from continuous holding periods, not from a date you nominate each year.
  • Gram equivalents for the nisab vary between institutions. This tool uses 87.48 g / 612.36 g (7.5 / 52.5 tola); others publish 85 g / 595 g, roughly a 3% difference. Override them if your authority differs.
  • Payments are not allocated per basis. A payment reduces both balances, because it is one real-world event viewed two ways.
  • Prices are taken on trust from your ledger. A stale price is flagged but still used; a missing one is an error, never a silent zero.

Troubleshooting

"No accounts are marked for zakat" — no Open directive carries beancount_zakat metadata. Note the value must be a quoted string: beancount_zakat: "asset", not beancount_zakat: asset.

"No gold price is available" — add a price directive for GLDTOLA (or whatever symbol you configured) dated on or before your report date. Prices dated after the cutoff are deliberately ignored.

"No price for XYZ ... could not be valued" — a tagged account holds a commodity with no price path to your operating currency. Add a price directive for it.

A slice shows "not running" for a long stretch — wealth was below that basis's nisab, so the hawl was reset and none of that time counts. The shaded band on the Calculation Detail chart shows exactly when. This is per basis: a reset under gold says nothing about silver.

The report is slow on a large ledger — the work grows with the number of distinct wealth levels rather than the number of transactions, so a long history of small changes is the expensive case. A synthetic 1,500-transaction ledger takes about two seconds. The dashboard caches the result, so only the first render after an edit pays for it.

Registration fails / the report does not appear — check the module path is beancount_zakat.fava_extension, and that pip install 'beancount-zakat[fava]' put the package on the same interpreter that runs Fava.

Privacy

  • Nothing leaves your machine. No network calls at runtime, no telemetry, no CDN — charts are drawn from data already on the page.
  • Nothing is written unless you ask. The only files produced are the CSV exports you request.
  • No ledger content in this repository. Every fixture and example is synthetic.
  • No server tracebacks in the browser. Errors go to the Fava log; the page shows a safe, actionable message instead.

How this is tested

Every release is checked on Python 3.10 through 3.13, on Linux, macOS and Windows. Beyond ordinary unit tests, the suite pins the things that would be easy to break quietly:

  • The figures reconcile. Yearly rows add up to the lifetime total exactly, not approximately, and the charts add up to the same net wealth the calculation used.
  • Randomised histories. The invariants are checked against thousands of randomly generated wealth timelines, not only the ones somebody thought to write by hand.
  • Whole ledgers, with included files, price directives, refunds, nisab breaks and long quiet stretches.
  • The engine really does run without Fava, which is checked rather than asserted.
  • The dashboard in a real browser, desktop and mobile, in both themes: no errors, no broken layout, keyboard navigation and chart interaction exercised end to end. The screenshots above come from that run.

Contributing

Bug reports and pull requests are welcome.

git clone https://github.com/WildeBeast2521/beancount-zakat
cd beancount-zakat
python -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
pytest

Never attach a real ledger to an issue or a pull request. Every fixture here is synthetic; reduce the problem to made-up account names and round numbers.

CONTRIBUTING.md has the full workflow. docs/architecture.md explains how the package is put together and why the calculation makes the choices it does — read it before changing anything in src/. SECURITY.md covers reporting a vulnerability privately.

License

beancount-zakat is released under the MIT license.

Disclaimer

This tool is for informational and record-keeping purposes only. Zakat rulings can vary by school of jurisprudence, asset type, debt treatment, personal circumstances, locality, and scholar. The software may contain errors and does not constitute religious, legal, tax, accounting, or financial advice. Verify the inputs and calculations and consult a qualified Islamic scholar and appropriate professional before acting.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

beancount_zakat-1.0.0.tar.gz (141.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

beancount_zakat-1.0.0-py3-none-any.whl (95.6 kB view details)

Uploaded Python 3

File details

Details for the file beancount_zakat-1.0.0.tar.gz.

File metadata

  • Download URL: beancount_zakat-1.0.0.tar.gz
  • Upload date:
  • Size: 141.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for beancount_zakat-1.0.0.tar.gz
Algorithm Hash digest
SHA256 f3f15bf32f63812f750e960efbd8c8b9807fb6316ac14afae966b236e79844a3
MD5 d96c51225862accc4a526e9c0f744d67
BLAKE2b-256 2b5ff75e5ae826c71c0961e32d90c91c07e7b1d8322c594957d2f094e5610e42

See more details on using hashes here.

Provenance

The following attestation bundles were made for beancount_zakat-1.0.0.tar.gz:

Publisher: release.yml on WildeBeast2521/beancount-zakat

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file beancount_zakat-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: beancount_zakat-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 95.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for beancount_zakat-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 19ae2fe81c91f8512e220a15db9e5567acdaf7a0a86d3f13c2118b514b53e7d8
MD5 a85ee1e3642f8b9d8378d7bc2367b37a
BLAKE2b-256 b58e68f32a5a1d3dcf07e10f9fc1ab6564c933fe77e8eebbe47a48edbb7ac54f

See more details on using hashes here.

Provenance

The following attestation bundles were made for beancount_zakat-1.0.0-py3-none-any.whl:

Publisher: release.yml on WildeBeast2521/beancount-zakat

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.0.2

2 files

1.0.1

2 files

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page