Skip to main content

Open-source Canadian payroll deductions calculator — CRA T4127 formulas for all provinces and territories

Project description

T4127 Engine

License: AGPL v3 Python 3.10+ Tests: 92,035 passing PDOC Validated Tax Year: 2026

Open-source Canadian payroll deductions calculator. Implements the CRA's T4127 formulas for all provinces and territories. Validated against 92,000+ test cases and penny-for-penny against the CRA's own PDOC calculator.

$1,000/week in Ontario, claim code 1:

  Federal tax:      $81.61
  Provincial tax:   $45.80
  CPP:              $55.50
  EI:               $16.30
  Total deductions: $199.21

Quick Example

from decimal import Decimal
from payroll_calc.calculator import calculate
from payroll_calc.data.loader import load_cra_data
from payroll_calc.config import Province, PayPeriod
from payroll_calc.models import DeductionRequest

cra = load_cra_data()

result = calculate(DeductionRequest(
    province=Province.ON,
    pay_period=PayPeriod.WEEKLY,
    gross_pay=Decimal("1000.00"),
), cra)

print(result.federal_tax)      # 81.61
print(result.provincial_tax)   # 45.80
print(result.cpp_total)        # 55.50
print(result.ei_premium)       # 16.30
print(result.total_deductions) # 199.21

Why This Exists

Every Canadian employer is legally required to calculate payroll deductions using formulas published by the Canada Revenue Agency in a document called the T4127.

The formulas are public. The problem is how they're published.

The CRA distributes its payroll data as 21 CSV files buried across multiple web pages, with inconsistent encoding (UTF-8 BOM, Windows-1252), comma-formatted numbers inside quoted fields, special tokens that require domain knowledge to interpret, and multi-row records that span 3 lines per province. The tax tables are locked inside PDF files that require scraping to validate against. The only official "calculator" is PDOC — a closed-source web app with no API, no batch mode, and no way to integrate it into anything.

There is no reference implementation. No open data API. No sample code. No test vectors.

A country with 2 million employers and a $1.2 trillion annual payroll base, and the government's answer is: here are some PDFs and a web form, good luck.

You have to wonder who benefits from this. It's certainly not small businesses and entrepreneurs, who are left choosing between paying a CPA hundreds of dollars a month, subscribing to payroll SaaS that charges per employee per pay run, or spending weeks deciphering a 70-page formula guide just to issue a paycheque. The complexity isn't accidental — it's a moat. Every layer of obscurity, every undocumented edge case, every PDF that should have been a CSV is another reason a small business owner gives up and hands their payroll to a vendor. The big accounting firms and payroll platforms aren't hurt by this — they thrive on it. They have teams of tax specialists and proprietary implementations they've built over decades. The opacity of the system is their competitive advantage, and the CRA's refusal to publish usable, machine-readable tax logic with reference implementations keeps that advantage locked in.

T4127 Engine exists to change this. If the CRA won't publish a reference implementation, the community can.

Part of the Canada Pay Freedom project. Licensed under AGPL-3.0.

Who is this for?

  • Payroll software developers who need a correct, auditable calculation engine they can integrate into their product
  • HRIS and HR tech builders who need a Canadian tax module without licensing a proprietary one
  • Accountants and bookkeepers who want to verify their software's output against a transparent, testable implementation
  • Small business owners who are tired of paying per-employee-per-month for what is fundamentally public arithmetic
  • Open-source projects that need Canadian payroll support without reinventing the T4127 from scratch
  • Anyone who believes tax calculation logic should be public infrastructure, not a proprietary black box

Installation

git clone https://github.com/asterling/T4127-Engine.git
cd T4127-Engine
pip install pydantic pyyaml

Download the CRA's published rate data (21 CSV files fetched directly from canada.ca):

python payroll_calc/download_cra.py

Verify everything works:

python -m pytest payroll_calc/tests/test_regression.py -v

That's it. 43 tests, all passing in under a second.

Usage

As a Python library

Load the CRA data once, then calculate as many pay periods as you need:

from decimal import Decimal
from payroll_calc.calculator import calculate
from payroll_calc.data.loader import load_cra_data
from payroll_calc.config import Province, PayPeriod
from payroll_calc.models import DeductionRequest

cra = load_cra_data()

# Basic calculation — just province, pay period, and gross pay
result = calculate(DeductionRequest(
    province=Province.AB,
    pay_period=PayPeriod.BIWEEKLY,
    gross_pay=Decimal("3200.00"),
), cra)

# With RPP contributions and union dues
result = calculate(DeductionRequest(
    province=Province.MB,
    pay_period=PayPeriod.BIWEEKLY,
    gross_pay=Decimal("3200.00"),
    rpp_contributions=Decimal("80.00"),
    union_dues=Decimal("25.00"),
), cra)

# With a bonus
result = calculate(DeductionRequest(
    province=Province.ON,
    pay_period=PayPeriod.WEEKLY,
    gross_pay=Decimal("1000.00"),
    bonus=Decimal("5000.00"),
), cra)
print(result.bonus_tax)  # tax on the $5,000 bonus

# Mid-year with YTD tracking (for accurate CPP/EI cap calculations)
result = calculate(DeductionRequest(
    province=Province.ON,
    pay_period=PayPeriod.WEEKLY,
    gross_pay=Decimal("1000.00"),
    ytd_cpp=Decimal("3800.00"),
    ytd_ei=Decimal("1050.00"),
    ytd_pensionable=Decimal("70000.00"),
), cra)
# CPP/EI will be reduced or zero if annual maximums are reached

As a REST API

uvicorn payroll_calc.main:app --port 8000

Interactive docs at http://localhost:8000/docs.

curl -X POST http://localhost:8000/calculate \
  -H "Content-Type: application/json" \
  -d '{
    "province": "ON",
    "pay_period": 52,
    "gross_pay": "1000.00",
    "federal_claim_code": 1,
    "provincial_claim_code": 1
  }'
{
  "federal_tax": "81.61",
  "provincial_tax": "45.80",
  "total_tax": "127.41",
  "cpp_total": "55.50",
  "cpp_base_portion": "46.17",
  "cpp2": "0.00",
  "ei_premium": "16.30",
  "total_deductions": "199.21",
  "bonus_tax": null,
  "annual_taxable_income": "51514.96",
  "basic_federal_tax": "4243.88",
  "annual_federal_tax": "4243.88",
  "basic_provincial_tax": "1781.51",
  "annual_provincial_tax": "2381.51"
}

Validate against the CRA's PDOC

Compare our output penny-for-penny against the CRA's official Payroll Deductions Online Calculator (requires Selenium + Chrome):

pip install selenium
python -m payroll_calc.pdoc_query --compare 1000 ON 52
Field                             PDOC         Ours       Diff
--------------------------------------------------------------
Federal tax               $     81.61  $     81.61     +0.00
Provincial tax            $     45.80  $     45.80     +0.00
CPP                       $     55.50  $     55.50     +0.00
EI                        $     16.30  $     16.30     +0.00
Total deductions          $    199.21  $    199.21     +0.00

PERFECT MATCH

Generate T4032 lookup tables

Download a T4032-style CSV lookup table (like the ones the CRA publishes as PDFs):

curl -o federal_weekly.csv http://localhost:8000/t4032/ON/52/federal.csv
curl -o provincial_biweekly.csv http://localhost:8000/t4032/ON/26/provincial.csv

Supported Provinces & Pay Periods

Provinces/territories: AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, SK, YT

Pay periods: Weekly (52), Biweekly (26), Semi-monthly (24), Monthly (12)

Claim codes: 0-10 (federal and provincial)

Not yet supported: Quebec provincial tax. Quebec administers its own provincial income tax through Revenu Québec using separate formulas (TP-1015.3), a separate pension plan (QPP instead of CPP), and a separate parental insurance plan (QPIP). Federal tax for Quebec employees is calculated (with the 16.5% abatement), but provincial deductions require a dedicated Revenu Québec implementation — this is on the roadmap.

Roadmap

  • Quebec provincial tax — Revenu Québec TP-1015.3 formulas, QPP, QPIP (the data is already downloaded, the formulas need implementing)
  • PyPI packagepip install t4127-engine for easy integration
  • Annual rate updates — automated workflow to pull new T4127 data when the CRA publishes each November
  • CPP/QPP edge cases — employees turning 18 or 70 mid-year, multi-jurisdiction workers
  • GitHub Actions CI — automated test runs on every push

See CONTRIBUTING.md if you want to help with any of these.


Deep Dive

Everything below is for people who want to understand how the engine works, what data it uses, and how it's tested.

What the CRA Actually Publishes

The T4127 Engine is built entirely from official CRA data. Here is exactly what the government provides and where — so you can verify we haven't made anything up.

The CRA publishes 21 CSV files as part of the T4127 package. They are not linked from a single page or documented in a consistent format. The download_cra.py script fetches all of them from canada.ca:

Claim Code Tables (13 files)

One CSV per jurisdiction — maps claim codes 0-10 to total claim amounts (TC) and pre-computed tax credits (K1/K1P):

File Jurisdiction
cc-fd-01-26e.csv Federal
cc-ab-01-26e.csv Alberta
cc-bc-01-26e.csv British Columbia
cc-mb-01-26e.csv Manitoba
cc-nb-01-26e.csv New Brunswick
cc-nl-01-26e.csv Newfoundland & Labrador
cc-ns-01-26e.csv Nova Scotia
cc-nt-01-26e.csv Northwest Territories
cc-nv-01-26e.csv Nunavut (CRA uses nv, not nu)
cc-on-01-26e.csv Ontario
cc-pei-01-26e.csv Prince Edward Island (CRA uses pei, not pe)
cc-sk-01-26e.csv Saskatchewan
cc-yt-01-26e.csv Yukon

Rate and Threshold Tables (8 files)

File T4127 Table Contents
rtsncmtrshldcnstnt-01-26e.csv Table 8.1 Tax brackets for all jurisdictions — 3 rows per province (thresholds, rates, constants). This is the core of the income tax calculation.
thrrtsmnts-01-26e.csv Table 8.2 Basic personal amounts, index rates, LCP credits, CEA, Ontario surtax (V1) tiers, and other jurisdiction-specific amounts.
cpp-qpp-br-01-26e.csv Table 8.4 CPP/QPP base contribution rates and annual maximums
cpp-qpp-ttl-01-26e.csv Table 8.3 CPP/QPP total rates, YMPE ($74,600), basic exemption ($3,500), max contributions
cpp-qpp-addntl-01-26e.csv Table 8.5 CPP/QPP first additional contribution (1% rate, $711 max)
cpp-qpp-scnd-addntl-01-26e.csv Table 8.6 CPP2 second additional contribution — YAMPE ($85,000), 4% rate, $416 max
ei-01-26e.csv Table 8.7 Employment Insurance rates and maximums (separate rows for QC and non-QC)
qpip-01-26e.csv Table 8.8 Quebec Parental Insurance Plan rates (for future Quebec support)

What the CRA does NOT publish

  • No reference implementation in any programming language
  • No machine-readable API for PDOC (the online calculator)
  • No test vectors or expected output for given inputs
  • No versioned data releases or changelogs for rate updates
  • No standardized format — the 21 CSVs use inconsistent encodings, non-standard province codes (nv for Nunavut, pei for PEI), comma-formatted numbers inside quoted fields, special tokens (BPAF, BPAMB, No claim amount), and multi-row records with no delimiter

All of these are parsed by data/loader.py, which handles every encoding quirk and format inconsistency we've encountered.

How the Calculation Works

The engine implements all 6 steps of the T4127 Option 1 formulas:

Step Module What It Computes
Prereq formulas/cpp.py CPP base + first additional + CPP2 contributions
Prereq formulas/ei.py EI premiums
1 formulas/annual_income.py Annual taxable income: A = P x (I - F - F2 - F5A - U1) - HD - F1
2-3 formulas/federal_tax.py Basic federal tax T3 and annual federal tax T1
4-5 formulas/provincial_tax.py Basic provincial tax T4 and annual provincial tax T2
6 formulas/per_period_tax.py Per-period deduction: T = (T1 + T2) / P + L

Supporting modules:

Module Purpose
formulas/credits.py Tax credits K1-K4 (federal) and K1P-K5P (provincial)
formulas/bpaf.py Dynamic basic personal amounts — BPAF clawback ($181,440-$258,482), BPAMB ($200,000-$400,000), BPAYT
formulas/province_specific.py Ontario surtax (V1), Ontario Health Premium (V2), Ontario/BC tax reductions (S), Alberta K5P
formulas/bonus.py Bonus/retroactive pay: TB = (T1+T2 with bonus) - (T1+T2 without bonus)

The main entry point is calculator.py, which orchestrates all steps in order and returns a DeductionResponse with every intermediate value.

Features

  • Full T4127 Option 1 — all 6 steps for salary, wages, and non-periodic payments
  • CPP/CPP2/EI — base CPP, first additional CPP, second additional CPP (CPP2), and EI
  • Dynamic BPAF/BPAMB/BPAYT — income-dependent basic personal amounts for federal, Manitoba, and Yukon
  • Province-specific rules — Ontario surtax (V1), Ontario Health Premium (V2), Ontario/BC tax reductions (S), Alberta supplemental credit (K5P), labour-sponsored funds credits (LCP) for MB, NB, NS, SK
  • Bonus/retroactive pay — differential tax calculation on non-periodic payments
  • T4032 table generation — produces CSV files matching the CRA's published lookup tables
  • Decimal precision — all arithmetic uses decimal.Decimal with CRA-specific rounding rules

Accuracy & Testing

92,035 tests

Test Suite Count What It Validates
test_regression.py 43 CRA data parsing, CPP/EI calculations, all provinces, all pay periods, claim code ordering, BPAF formulas, bonus tax
test_t4032_exhaustive.py 91,992 Every row in all 8 T4032ON PDF tables (4 pay periods x federal + provincial), tested at midpoint + 2 random points per range, across all claim codes

PDOC penny-for-penny match

Gross Pay Province Period Federal Tax Provincial Tax Total Deductions PDOC Match
$1,000/wk ON 52pp $81.61 $45.80 $199.21 Exact
$3,000/wk ON 52pp $514.60 $301.03 $1,039.03 Exact
$5,000/wk ON 52pp $1,073.23 $690.74 $2,138.97 Exact
$20,000/mo ON 12pp $4,172.15 $2,654.48 $8,325.28 Exact

Why T4032 tables show larger differences at high incomes

Below YMPE (~$74,600/year annualized), our calculator and the T4032 PDF tables agree within $1.50/period. Above YMPE, differences grow to $5-$70 because the T4032 tables don't properly handle the CPP annual cap. The CRA acknowledges this:

"If at any point during the year, the employee reaches the YMPE of $74,600 [...] we recommend using the PDOC for more accurate calculations."

Our calculator matches PDOC exactly — the T4032 discrepancies are a limitation of the lookup tables, not our formulas.

API Reference

POST /calculate

All fields except province, pay_period, and gross_pay are optional and default to sensible values (claim code 1, zero deductions, zero YTD).

Request body:

Field Type Default Description
province string required AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, SK, YT
pay_period int required 52, 26, 24, or 12
gross_pay decimal required Gross remuneration for the pay period
federal_claim_code int 1 Federal claim code (0-10)
provincial_claim_code int 1 Provincial claim code (0-10)
pensionable_earnings decimal gross_pay If different from gross pay
insurable_earnings decimal gross_pay If different from gross pay
rpp_contributions decimal 0 RPP/RRSP contributions per period
union_dues decimal 0 Union dues per period
prescribed_zone decimal 0 Annual prescribed zone deduction
annual_deductions decimal 0 Annual deductions (child care, support payments)
additional_tax decimal 0 Additional tax per period (TD1 request)
bonus decimal 0 Bonus or retroactive pay this period
ytd_cpp decimal 0 Year-to-date CPP contributions
ytd_cpp2 decimal 0 Year-to-date CPP2 contributions
ytd_ei decimal 0 Year-to-date EI premiums
ytd_pensionable decimal 0 Year-to-date pensionable earnings
dependants_for_reduction decimal 0 Ontario tax reduction dependant amount
cpp_months int 12 Months CPP contributions required

Response body:

Field Description
federal_tax Federal income tax for the period
provincial_tax Provincial income tax for the period
total_tax Combined federal + provincial
cpp_total CPP contribution (base + first additional)
cpp_base_portion Base CPP portion (used in K2 credit)
cpp2 CPP2 second additional contribution
ei_premium Employment insurance premium
total_deductions Sum of all deductions
bonus_tax Tax on the bonus (null if no bonus)
annual_taxable_income Annualized taxable income (A)
basic_federal_tax Basic federal tax (T3)
annual_federal_tax Annual federal tax (T1)
basic_provincial_tax Basic provincial tax (T4)
annual_provincial_tax Annual provincial tax (T2)

GET /t4032/{province}/{pay_period}/{table_type}.csv

Generate a T4032-style CSV. table_type is federal or provincial. Returns CSV with columns: From, Less than, CC 0 through CC 10.

GET /provinces

List all supported provinces.

GET /claim-codes/{jurisdiction}

Claim code table for federal or a province code (e.g., ON).

Project Structure

T4127-Engine/
├── README.md
├── LICENSE                          AGPL-3.0
├── CONTRIBUTING.md
├── pyproject.toml
│
└── payroll_calc/
    ├── __init__.py                  Package init, __version__
    ├── calculator.py                Main engine — orchestrates all T4127 steps
    ├── config.py                    Province/PayPeriod enums, CRA code mappings
    ├── models.py                    Pydantic request/response models
    ├── rounding.py                  CRA-specific rounding (ROUND_HALF_UP, truncation)
    ├── main.py                      FastAPI app entry point
    ├── download_cra.py              Downloads all CRA CSVs and PDFs from canada.ca
    ├── pdoc_query.py                Selenium PDOC scraper for validation
    │
    ├── data/
    │   ├── schema.py                Dataclasses: TaxBracket, ClaimCode, CppParams, etc.
    │   └── loader.py                CSV parser — handles all CRA encoding quirks
    │
    ├── formulas/
    │   ├── annual_income.py         Step 1: annual taxable income (A)
    │   ├── federal_tax.py           Steps 2-3: T3, T1
    │   ├── provincial_tax.py        Steps 4-5: T4, T2
    │   ├── per_period_tax.py        Step 6: per-period tax (T)
    │   ├── credits.py               Tax credits K1-K4, K1P-K5P
    │   ├── cpp.py                   CPP/CPP2 contributions
    │   ├── ei.py                    EI premiums
    │   ├── bpaf.py                  Dynamic BPAF/BPAMB/BPAYT
    │   ├── province_specific.py     ON surtax/OHP, BC reduction, AB K5P
    │   └── bonus.py                 Bonus/retroactive pay tax
    │
    ├── tables/
    │   └── t4032_generator.py       Generates T4032-style CSV lookup tables
    │
    ├── api/
    │   └── routes.py                FastAPI route definitions
    │
    ├── tests/
    │   ├── test_regression.py       43 regression tests
    │   ├── test_t4032_exhaustive.py 91,992 exhaustive PDF validation tests
    │   ├── extract_pdf_reference.py PDF table extractor
    │   └── reference_data/          Extracted T4032ON tables (8 JSON files)
    │
    └── cra_data/                    Downloaded CRA data (gitignored)
        └── 2026/
            ├── csvs/                21 CSV files
            └── pdfs/                12 PDF files

CRA Data Sources

Source Edition URL
T4127 Payroll Deductions Formulas 122nd, Jan 2026 canada.ca
T4032ON Payroll Deductions Tables Jan 2026 canada.ca
PDOC Online Calculator 2026 canada.ca

Contributing

See CONTRIBUTING.md for development setup and PR guidelines.

License

AGPL-3.0 — free to use, modify, and deploy. If you modify and deploy it as a service, you must share your changes.

Project details


Download files

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

Source Distribution

t4127_engine-0.1.0.tar.gz (57.6 kB view details)

Uploaded Source

Built Distribution

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

t4127_engine-0.1.0-py3-none-any.whl (58.0 kB view details)

Uploaded Python 3

File details

Details for the file t4127_engine-0.1.0.tar.gz.

File metadata

  • Download URL: t4127_engine-0.1.0.tar.gz
  • Upload date:
  • Size: 57.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for t4127_engine-0.1.0.tar.gz
Algorithm Hash digest
SHA256 7c7fecfa97fe884584f88fc03b45c2ed82514f13c1d6ba3331362f087d33a8e8
MD5 d6bed20218e82a8f1f816b24d68f3514
BLAKE2b-256 8103a6034551dd42f5dce5d8d4fdc81c945b6395c7420f3f659ca78b2ea82475

See more details on using hashes here.

File details

Details for the file t4127_engine-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: t4127_engine-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 58.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.9

File hashes

Hashes for t4127_engine-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb9a0342ffb2a5023172e8ff5cb414f52eb2448474d59f91d7060178b642aaac
MD5 a71f4c59b912ff030b12e9792f8d1672
BLAKE2b-256 2d820dd6938f37efd06a057934b0efd00d2c939712be7e5ae6644f126fdd6540

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page