Philippine BIR income tax computation for Python. The graduated tables are data; the computation is one small function over them.
No dependencies, typed, Decimal throughout. Python 3.9+.
pip install bir-taxesfrom birtax import compute_income_tax
result = compute_income_tax(1_000_000)
result.tax_due # Decimal('152500.00')
result.marginal_rate # Decimal('0.25')
result.effective_rate # Decimal('0.1525')
print(result.explain())Table: Graduated income tax, 2023 onwards
Taxable income: PHP 1,000,000.00
Bracket: 800,000 to 2,000,000: 102,500 + 25% of the excess over 800,000
Computation: 102,500.00 + 25% x 200,000.00
Tax due: PHP 152,500.00
Effective rate: 15.25%
Marginal rate: 25%
If you only want the number:
from birtax import tax_due
tax_due("1_250_000") # Decimal('215000.00')
tax_due(300_000, year=2021) # Decimal('10000.00') -- the 2018-2022 tabletaxable_income is net of every deduction and exemption. This library
applies a table and nothing else — it does not compute deductions, the ₱90,000
13th-month exclusion, SSS/PhilHealth/Pag-IBIG contributions, the 8% gross
receipts option, or withholding per payroll period.
$ birtax 1,250,000
$ birtax --json 300000 1000000
$ birtax --year 2021 500000
$ birtax --table 2027-proposed --show-tableRA 10963 (TRAIN), sec. 24(A)(2)(b). No change took effect for 2026.
| Annual taxable income | Tax due |
|---|---|
| Not over ₱250,000 | None |
| Over ₱250,000 but not over ₱400,000 | 15% of the excess over ₱250,000 |
| Over ₱400,000 but not over ₱800,000 | ₱22,500 + 20% of the excess over ₱400,000 |
| Over ₱800,000 but not over ₱2,000,000 | ₱102,500 + 25% of the excess over ₱800,000 |
| Over ₱2,000,000 but not over ₱8,000,000 | ₱402,500 + 30% of the excess over ₱2,000,000 |
| Over ₱8,000,000 | ₱2,202,500 + 35% of the excess over ₱8,000,000 |
RA 10963 (TRAIN), sec. 24(A)(2)(a). Kept for amended returns and back-year
audits, reachable as TABLE_2018_2022 or year=2018..2022.
| Annual taxable income | Tax due |
|---|---|
| Not over ₱250,000 | None |
| Over ₱250,000 but not over ₱400,000 | 20% of the excess over ₱250,000 |
| Over ₱400,000 but not over ₱800,000 | ₱30,000 + 25% of the excess over ₱400,000 |
| Over ₱800,000 but not over ₱2,000,000 | ₱130,000 + 30% of the excess over ₱800,000 |
| Over ₱2,000,000 but not over ₱8,000,000 | ₱490,000 + 32% of the excess over ₱2,000,000 |
| Over ₱8,000,000 | ₱2,410,000 + 35% of the excess over ₱8,000,000 |
Bills pending in both chambers (the "Expanded Income Tax Exemption Act of
2026") would lift the exempt ceiling from ₱250,000 to ₱350,000 and keep the
20/25/30/35% rates. It is not enacted: table_for_year will never return
it, and you must opt in by name.
from birtax import TABLE_2027_PROPOSED, tax_due
tax_due(380_000, table=TABLE_2027_PROPOSED) # Decimal('4500.00')| Annual taxable income | Tax due |
|---|---|
| Not over ₱350,000 | None |
| Over ₱350,000 but not over ₱400,000 | 15% of the excess over ₱350,000 |
| Over ₱400,000 but not over ₱800,000 | ₱22,500 + 20% of the excess over ₱400,000 |
| Over ₱800,000 but not over ₱2,000,000 | ₱102,500 + 25% of the excess over ₱800,000 |
| Over ₱2,000,000 but not over ₱8,000,000 | ₱402,500 + 30% of the excess over ₱2,000,000 |
| Over ₱8,000,000 | ₱2,202,500 + 35% of the excess over ₱8,000,000 |
This table has a cliff at ₱400,000. Moving the ceiling to ₱350,000 shrinks the 15% band to ₱50,000, which yields ₱7,500 — but the next row still starts at the old ₱22,500. Earning one more peso above ₱400,000 costs ₱15,000 in tax. The library encodes the draft as written rather than silently "fixing" it, and reports the gap:
TABLE_2027_PROPOSED.discontinuities() # ((<400k row>, <800k row>, Decimal('15000')), ) TABLE_2023_ONWARDS.discontinuities() # () -- enacted law is continuousIf the final law recomputes the fixed amounts instead, that ₱22,500 becomes ₱7,500 and the rest shift down with it. Build your own
TaxTableuntil then.
Amounts are Decimal. int, str, and Decimal are exact; float is
converted through its repr, so 0.1 means one tenth — but prefer strings for
centavo-precise input. Results round half-up to centavos, as BIR forms do; pass
rounding=None for full precision or rounding=PESO for whole pesos.
from birtax import Bracket, TaxTable, tax_due
local = TaxTable(
name="Some future table",
brackets=(
Bracket(0, 350_000, 0, "0"),
Bracket(350_000, 400_000, 0, "0.15"),
Bracket(400_000, None, 7_500, "0.20"),
),
)
tax_due(500_000, table=local)TaxTable refuses gaps, overlaps, a first bracket that does not start at zero,
and a last bracket that is not open-ended — a mistyped table fails at import,
not in a payslip.
Every expected figure in the test suite is hand-computed from the published
table, never captured from this library's output. Tables cite their statute in
.authority.
This is not tax advice. Verify against the BIR issuance that applies to you.
See CONTRIBUTING.md. New tables are welcome — cite the law, add hand-computed cases, and never delete a superseded table.
MIT.