payroll.gross-to-net Unreviewed
A UK payslip for one pay period: gross pay, pension, PAYE tax, National Insurance, student loans and net pay.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 8 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Status: needs review by a qualified payroll professional before it is published.
One employee's payslip for one pay period. This capability contains no rates, no thresholds and no rounding of its own: each line comes from the capability that owns it, and what is left here is the order of operations, which is where payslips usually go wrong.
For example
gross_to_net(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio…)→ gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa… 3,000 pounds a month on 1257L, category A, nothing elsegross_to_net(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, …)→ gross £3,000.00, pension employee £99.20, pension employer £74.40, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £49.00, postgraduate … relief at source pension at the minimums, plan 2 and a postgraduate loangross_to_net(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio…)→ gross £3,000.00, pension employee £150.00, pension employer £90.00, taxable pay £2,850.00, tax £360.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate … a net pay arrangement comes off before tax but not before NI
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def gross_to_net(input: PayslipInput) -> Payslip
| input | PayslipInput | |
| returns | Payslip |
The types it declares, generated into your project
@dataclass(frozen=True)
class PayslipInput:
"""One employee's pay for one period, and what the payroll knows about them."""
#: gross pay for the period, all of it taxable and NI-able
gross: Money
frequency: PayFrequency
#: decides the tax year, tax period and every rate
pay_date: str
#: as issued by HMRC, e.g. 1257L, S1257L, BR, 1257L M1
tax_code: str
#: taxable pay already paid this tax year (including a P45's figure)
previous_taxable_pay_to_date: Money
#: tax already deducted this tax year (including a P45's figure)
previous_tax_to_date: Money
#: National Insurance category letter, usually A
ni_category: str
#: every plan the employee repays; empty for none
student_loan_plans: List[StudentLoanPlan]
#: null when the employee is not in a workplace pension
pension: Optional[PensionScheme]
@dataclass(frozen=True)
class Payslip:
"""Every line of the payslip, and the year-to-date figures the next period needs."""
gross: Money
#: deducted from pay (net of basic-rate relief under relief at source)
pension_employee: Money
#: paid by the employer on top; not deducted
pension_employer: Money
#: gross less a net pay arrangement pension deduction
taxable_pay: Money
#: PAYE income tax; negative is a refund
tax: Money
employee_ni: Money
#: paid by the employer on top; not deducted
employer_ni: Money
#: undergraduate plans
student_loan: Money
postgraduate_loan: Money
#: what the employee is paid
net: Money
#: previousTaxablePayToDate plus taxablePay
taxable_pay_to_date: Money
#: previousTaxToDate plus tax
tax_to_date: Money
#: tax week or month the payment falls in
period: int
#: e.g. 2026/27
tax_year: str
Your code names it in one line, in the file that uses it
from fune.payroll.gross_to_net import gross_to_net # payroll.gross-to-net@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_gross_to_net_types import Payslip, PayslipInput
from .payroll_income_tax import income_tax ← from payroll.income-tax ^1.0.0 · built alongside by fune
from .payroll_national_insurance import national_insurance ← from payroll.national-insurance ^1.0.0 · built alongside by fune
from .payroll_pension_auto_enrolment import pension_contributions ← from payroll.pension-auto-enrolment ^1.0.0 · built alongside by fune
from .payroll_student_loan import student_loan ← from payroll.student-loan ^1.0.0 · built alongside by fune
from .payroll_tax_period import tax_period ← from payroll.tax-period ^1.0.0 · built alongside by fune
def gross_to_net(input: PayslipInput) -> Payslip:
"""One period's payslip, in the order a UK payroll works it out.
The order matters because each deduction is taken from a different figure:
a net pay arrangement pension comes off pay before tax but not before
National Insurance; tax is on taxable pay to date; NI and student loans are
on this period's gross pay alone; relief at source pension comes off net pay.
"""
gross = input.gross
if gross.currency != "GBP":
raise ValueError("payroll amounts must be in GBP, received %s for gross" % (gross.currency,))
when = tax_period(input.pay_date, input.frequency)
pension_employee = 0
pension_employer = 0
before_tax = 0
if input.pension is not None:
pension = pension_contributions(gross, input.frequency, input.pension, input.pay_date)
pension_employee = pension.employee_deduction.minor
pension_employer = pension.employer.minor
if pension.deduct_before_tax:
before_tax = pension.employee_deduction.minor
taxable_pay = money(gross.minor - before_tax, "GBP")
tax = income_tax(input.tax_code, input.frequency, when.period, taxable_pay, input.previous_taxable_pay_to_date, input.previous_tax_to_date, input.pay_date)
ni = national_insurance(gross, input.ni_category, input.frequency, input.pay_date, None)
# An employee with more than one undergraduate plan repays once, against the
# lowest threshold, which is the largest of the per-plan deductions; a
# postgraduate loan is repaid alongside it.
undergraduate = 0
postgraduate = 0
seen: List[str] = []
for plan in input.student_loan_plans:
if plan in seen:
raise ValueError("student loan plan %s is listed twice" % (plan,))
seen.append(plan)
deduction = student_loan(gross, plan, input.frequency, input.pay_date).minor
if plan == "postgraduate":
postgraduate = deduction
elif deduction > undergraduate:
undergraduate = deduction
net = gross.minor - pension_employee - tax.tax.minor - ni.employee.minor - undergraduate - postgraduate
return Payslip(
gross=gross,
pension_employee=money(pension_employee, "GBP"),
pension_employer=money(pension_employer, "GBP"),
taxable_pay=taxable_pay,
tax=tax.tax,
employee_ni=ni.employee,
employer_ni=ni.employer,
student_loan=money(undergraduate, "GBP"),
postgraduate_loan=money(postgraduate, "GBP"),
net=money(net, "GBP"),
taxable_pay_to_date=tax.pay_to_date,
tax_to_date=tax.tax_to_date,
period=when.period,
tax_year=when.tax_year,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 6 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add payroll.gross-to-net
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./payroll.gross-to-net-1.0.1-python.fune, or fetch it from a terminal with fune pull payroll.gross-to-net@1.0.1:python.
The whole function, every language, is one file too: payroll.gross-to-net-1.0.1.fune, 31,769 bytes, sha256 c37221261111539c198a5a8f58c94c4736fb0e9603bb5fe8e1d34290fb6a1a56. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before payroll.gross-to-net
after — your function gets the result and the arguments, and returns the final result.
# fune: after payroll.gross-to-net
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace money.amount in payroll.gross-to-net
# fune: replace payroll.income-tax in payroll.gross-to-net
# fune: replace payroll.national-insurance in payroll.gross-to-net
# fune: replace payroll.pension-auto-enrolment in payroll.gross-to-net
# fune: replace payroll.student-loan in payroll.gross-to-net
# fune: replace payroll.tax-period in payroll.gross-to-net
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show payroll.gross-to-net --steps.
# fune: step payroll.gross-to-net after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| 3,000 pounds a month on 1257L, category A, nothing else | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… | → | gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa… |
| relief at source pension at the minimums, plan 2 and a postgraduate loan | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, … | → | gross £3,000.00, pension employee £99.20, pension employer £74.40, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £49.00, postgraduate … |
| a net pay arrangement comes off before tax but not before NI | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… | → | gross £3,000.00, pension employee £150.00, pension employer £90.00, taxable pay £2,850.00, tax £360.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate … |
| a Scottish taxpayer with plans 1 and 2 repays once, at plan 1's lower threshold | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code S1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2,… | → | gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £392.27, employee ni £156.16, employer ni £387.45, student loan £68.00, postgraduate lo… |
| weekly pay under the allowance and the primary threshold: only employer NI | gross £200.00, frequency weekly, pay date 2025-04-11, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pension — | → | gross £200.00, pension employee £0.00, pension employer £0.00, taxable pay £200.00, tax £0.00, employee ni £0.00, employer ni £15.60, student loan £0.00, postgraduate loan £0.00, … |
| a student loan plan listed twice is an error | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, … | → | error: listed twice |
| gross pay in euros is an error | gross €3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… | → | error: must be in GBP |
| a code HMRC does not issue is an error | gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257Q, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… | → | error: unrecognised tax code |
More from the author
1. **Tax period** from the pay date and frequency (`payroll.tax-period`). 2. **Pension** (`payroll.pension-auto-enrolment`), if the employee is in a scheme. Under a *net pay arrangement* the employee's contribution comes off pay before tax; under *relief at source* it is taken from net pay, already reduced by basic-rate relief, and taxable pay is untouched. Neither reduces National Insurance or student loan pay: only salary sacrifice does that, and salary sacrifice is a change to gross pay, so give the reduced gross. 3. **Income tax** on taxable pay (`payroll.income-tax`), cumulative or week 1 / month 1 as the code says. 4. **National Insurance** on this period's gross (`payroll.national-insurance`), by the ordinary per-period method. Directors on the annual earnings period method should call `payroll.national-insurance` directly. 5. **Student loans** on this period's gross (`payroll.student-loan`). An employee with more than one undergraduate plan repays once, at the plan with the lowest threshold, which is the largest single-plan deduction; a postgraduate loan is repaid on top. 6. **Net pay** = gross - pension deduction - tax - employee NI - student loans.
A tax refund makes `tax` negative and increases net pay. Employer NI and the employer pension contribution are reported but not deducted.
Out of scope: statutory payments (give them as part of gross), benefits in kind, attachment of earnings orders, salary sacrifice arithmetic, and deductions after net pay.
Sources: the rules are those of the capabilities listed above, each of which cites its own. The order of deductions follows HMRC's "Employer further guide to PAYE and NICs" (CWG2), https://www.gov.uk/government/publications/cwg2-further-guide-to-paye-and-national-insurance-contributions, and for student loans ("when the employee is on more than one plan, start deductions for the plan with the lowest recovery threshold"; loans are worked out on the same pay as secondary Class 1 NICs), HMRC's "Student loan and postgraduate loan repayment guidance for employers", https://www.gov.uk/guidance/special-rules-for-student-loans.
## Before you rely on this
**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a payroll professional review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified payroll professional has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
## Notices
Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
1.0.1 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 183 |
| README.md | 3,834 |
| impl/python.py | 3,098 |
| impl/rust.rs | 5,882 |
| impl/typescript.ts | 3,042 |
| vectors.json | 8,606 |