Functional Weave
Code in Python

charity.higher-rate-relief

A Gift Aid donor's extra tax relief at higher, intermediate, additional and top rates, by extending the tax bands.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 16 tests, run in TypeScript, Python and Rust.

Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax 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 tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

What a donor's Gift Aid donations save the donor in their own tax, on top of the basic rate tax the charity reclaims. Give it the donor's taxable income for the year, the total Gift Aid donations they made in it, and where they pay income tax.

ITA 2007 s414(2)(b) treats the basic rate limit and the higher rate limit as raised by the grossed up donations. For a Scottish taxpayer the upper limit of the Scottish basic rate and the limits of every Scottish rate above it are raised too, but **not the starter rate limit**. Income that the wider bands move from a higher rate into a lower one is the relief:

For example

  • higher_rate_relief(£60,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00 higher rate taxpayer, £800 given: £1,000 gross, the donor saves another £200
  • higher_rate_relief(£38,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £7,660.00, tax after £7,600.00, extra relief £60.00, net cost £740.00 income just £300 into the higher band: only £300 moves, relief £60 not £200
  • higher_rate_relief(£150,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £53,703.00, tax after £53,453.00, extra relief £250.00, net cost £550.00 additional rate taxpayer: the higher rate limit moves too, relief 25% of gross

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 higher_rate_relief(taxable_income: Money, donations: Money, region: TaxRegion, on_date: str) -> HigherRateRelief
taxable_incomeMoneyincome after allowances and reliefs for the year, in GBP, before any Gift Aid adjustment
donationsMoneytotal Gift Aid donations the donor made in the tax year: the amounts actually paid
regionTaxRegionwhere the donor pays income tax
on_datedateany date in the tax year of the gifts
returnsHigherRateRelief

The types it declares, generated into your project

TaxRegion = Literal["rest-of-uk", "scotland", "wales"]

@dataclass(frozen=True)
class HigherRateRelief:
    """What the donor's Gift Aid is worth to the donor, beyond the charity's claim."""

    #: donations grossed up at the UK basic rate: the band extension
    gross_donations: Money
    #: the tax the charity reclaims: grossDonations less donations
    basic_rate_relief: Money
    #: tax on taxableIncome with the ordinary bands, rounded down to the penny
    tax_before: Money
    #: taxBefore less extraRelief
    tax_after: Money
    #: the donor's own saving from the extended bands, rounded down to the penny
    extra_relief: Money
    #: donations less extraRelief: what the gifts cost the donor
    net_cost: Money

Your code names it in one line, in the file that uses it

from fune.charity.higher_rate_relief import higher_rate_relief  # charity.higher-rate-relief@^1
impl/python.py · 88 lines · open · raw

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, Optional

from .charity_gift_aid import gift_aid  ← from charity.gift-aid ^1.0.0 · built alongside by fune
from .charity_higher_rate_relief_types import HigherRateRelief, TaxRegion
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .payroll_income_tax_data import INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY, IncomeTaxBand  ← payroll.income-tax’s rule data (^1.0.0) · built alongside by fune

REGIONS = ("rest-of-uk", "scotland", "wales")


def _bands_for(region: str, on_date: str) -> List[IncomeTaxBand]:
    rows = [
        b for b in INCOME_TAX_BANDS
        if b.region == region and b.valid_from <= on_date and (b.valid_to is None or on_date <= b.valid_to)
    ]
    if not rows:
        # A history=current build only carries this year's bands; an older year
        # would otherwise be answered with the wrong bands.
        froms = [b.valid_from for b in INCOME_TAX_BANDS if b.region == region]
        if INCOME_TAX_BANDS_HISTORY != "full" and froms:
            earliest = min(froms)
            if on_date < earliest:
                raise ValueError(
                    "no income tax bands for %s on %s: this build was installed with history=%s, so it only carries rules from %s. "
                    "Reinstall with history=full for earlier tax years." % (region, on_date, INCOME_TAX_BANDS_HISTORY, earliest)
                )
        raise ValueError("no income tax bands for %s on %s" % (region, on_date))
    return sorted(rows, key=lambda b: b.band)


def _basic_band_index(bands: List[IncomeTaxBand], region: str, on_date: str) -> int:
    for i, b in enumerate(bands):
        if b.basic_rate:
            return i
    raise ValueError("no basic rate band for %s on %s" % (region, on_date))


def _scaled_tax(income: int, bands: List[IncomeTaxBand], extend_from: int, extension: int) -> int:
    """Tax on income in pence, times 10000 (pence x basis points), unrounded."""
    total = 0
    lower = 0
    for i, band in enumerate(bands):
        if income <= lower:
            break
        upper: Optional[int] = None if band.up_to is None else band.up_to * 100 + (extension if i >= extend_from else 0)
        top = income if upper is None or income < upper else upper
        if top > lower:
            total += (top - lower) * band.basis_points
        if upper is None:
            break
        lower = upper
    return total


def higher_rate_relief(taxable_income: Money, donations: Money, region: TaxRegion, on_date: str) -> HigherRateRelief:
    """The extra relief a Gift Aid donor gets through their own tax: ITA 2007
    s414(2)(b) raises the basic rate limit and the higher rate limit (for a
    Scottish taxpayer, the Scottish basic rate limit and every limit above it,
    but not the starter rate limit) by the grossed up donations.
    """
    if taxable_income.currency != "GBP":
        raise ValueError("taxableIncome must be in GBP, received %s" % (taxable_income.currency,))
    if taxable_income.minor < 0:
        raise ValueError("taxableIncome must not be negative, received %d" % (taxable_income.minor,))
    if region not in REGIONS:
        raise ValueError('unknown tax region "%s": expected rest-of-uk, scotland or wales' % (region,))
    grossed = gift_aid(donations, on_date)
    bands = _bands_for(region, on_date)
    basic = _basic_band_index(bands, region, on_date)
    uk_bands = _bands_for("rest-of-uk", on_date)
    uk_basic = uk_bands[_basic_band_index(uk_bands, "rest-of-uk", on_date)]
    if bands[basic].basis_points != uk_basic.basis_points:
        # s414A then adjusts the donor's tax by the rate difference; not modelled.
        raise ValueError(
            "the %s basic rate differs from the UK basic rate on %s; the section 414A adjustment is not supported" % (region, on_date)
        )
    before = _scaled_tax(taxable_income.minor, bands, basic, 0)
    after = _scaled_tax(taxable_income.minor, bands, basic, grossed.gross.minor)
    tax_before = before // 10000
    extra = (before - after) // 10000
    return HigherRateRelief(
        gross_donations=grossed.gross,
        basic_rate_relief=grossed.tax_reclaimable,
        tax_before=money(tax_before, "GBP"),
        tax_after=money(tax_before - extra, "GBP"),
        extra_relief=money(extra, "GBP"),
        net_cost=money(donations.minor - extra, "GBP"),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 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 charity.higher-rate-relief
Download for Python charity.higher-rate-relief-1.0.0-python.fune · 18,172 bytes sha256 06b94ec2786123e88897e7cf882d191abd7bbd0af92f2fe14eb28d67182d6ff3

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./charity.higher-rate-relief-1.0.0-python.fune, or fetch it from a terminal with fune pull charity.higher-rate-relief@1.0.0:python.

The whole function, every language, is one file too: charity.higher-rate-relief-1.0.0.fune, 28,062 bytes, sha256 e2ac4b8686231fef1cf45c66ac8d43723d57152a6aeef90a157fc4660e0cc9cf. 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 charity.higher-rate-relief

after — your function gets the result and the arguments, and returns the final result.

# fune: after charity.higher-rate-relief

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 charity.gift-aid in charity.higher-rate-relief
# fune: replace money.amount in charity.higher-rate-relief
# fune: replace payroll.income-tax in charity.higher-rate-relief

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 charity.higher-rate-relief --steps.

# fune: step charity.higher-rate-relief 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.

CaseArgumentsExpected
higher rate taxpayer, £800 given: £1,000 gross, the donor saves another £200 £60,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00
income just £300 into the higher band: only £300 moves, relief £60 not £200 £38,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £7,660.00, tax after £7,600.00, extra relief £60.00, net cost £740.00
additional rate taxpayer: the higher rate limit moves too, relief 25% of gross £150,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £53,703.00, tax after £53,453.00, extra relief £250.00, net cost £550.00
basic rate taxpayer: no extra relief £20,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £4,000.00, tax after £4,000.00, extra relief £0.00, net cost £800.00
no taxable income: no tax, no extra relief £0.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £0.00, tax after £0.00, extra relief £0.00, net cost £800.00
£10.01 given: 20% of £12.51 is 250.2p, rounded down to 250p £60,000.00, £10.01, rest-of-uk, 2026-06-01 → gross donations £12.51, basic rate relief £2.50, tax before £16,460.00, tax after £16,457.50, extra relief £2.50, net cost £7.51
Scottish higher rate taxpayer 2026-27: 42% less 20%, relief £220 on £1,000 gross £40,000.00, £800.00, scotland, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £10,061.45, tax after £9,841.45, extra relief £220.00, net cost £580.00
Scottish intermediate rate taxpayer: relief 1%, the starter band is not extended £20,000.00, £800.00, scotland, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £3,990.77, tax after £3,980.77, extra relief £10.00, net cost £790.00
Scottish top rate taxpayer 2023-24: the top rate threshold moves as well, 27% relief £130,000.00, £800.00, scotland, 2023-12-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £48,160.88, tax after £47,890.88, extra relief £270.00, net cost £530.00
a Welsh taxpayer uses the UK limits £60,000.00, £800.00, wales, 2024-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00
Show the other 6 tests
CaseArgumentsExpected
no donations: nothing changes £60,000.00, £0.00, rest-of-uk, 2026-06-01 → gross donations £0.00, basic rate relief £0.00, tax before £16,460.00, tax after £16,460.00, extra relief £0.00, net cost £0.00
a tax year before the bands on file is an error £60,000.00, £800.00, rest-of-uk, 2022-06-01 → error: no income tax bands for rest-of-uk on 2022-06-01
an unknown region is an error £60,000.00, £800.00, england, 2026-06-01 → error: unknown tax region "england"
negative taxable income is an error -£0.01, £800.00, rest-of-uk, 2026-06-01 → error: taxableIncome must not be negative
taxable income in euros is an error €60,000.00, £800.00, rest-of-uk, 2026-06-01 → error: taxableIncome must be in GBP
negative donations are an error £60,000.00, -£1.00, rest-of-uk, 2026-06-01 → error: donation must not be negative

More from the author

- a higher rate taxpayer giving £800 (£1,000 gross) saves another £200; - an additional rate taxpayer saves £250, because the additional rate threshold moves as well; - a Scottish intermediate rate taxpayer saves 1% of the gross, a Scottish higher rate taxpayer 22%; - someone only £300 into the higher band saves 20% of £300, not of £1,000.

The relief is worked out as the difference between the tax on the same income with the ordinary and the extended bands, in exact pence x basis points, and rounded down to the penny once. `taxAfter` is `taxBefore` less that relief.

## Where the bands come from

The bands are **payroll.income-tax's own dated band data** (region, rate, upper limit, which band is the basic rate), imported from its data module rather than copied, so a new tax year added there reaches this capability too. That data starts in 2023-24, so earlier tax years are an error. Its `upTo` figures are the statutory limits on taxable income (basic rate limit £37,700, higher rate limit £125,140; the Scottish limits by year). Grossing up uses the UK basic rate from charity.gift-aid, as s415 requires, for Scottish and Welsh taxpayers too.

## What it does not do

- **The personal allowance taper.** Gift Aid also reduces adjusted net income (ITA 2007 s58), which can restore personal allowance lost above £100,000 and is worth far more there. `taxableIncome` is taken as given, after allowances; work the allowance out first with the donations deducted from adjusted net income, then call this. - Savings and dividend income, which have their own rates. - s414A, the adjustment when a devolved basic rate differs from the UK basic rate. The Scottish and Welsh basic rates are 20% in every year on file, so it never applies; if the data ever says otherwise this is an error rather than a wrong answer. - Carry-back of gifts to the previous tax year (s426).

## Sources

- Income Tax Act 2007 s414 (band extension, including the Scottish limits) and s415 (grossed up at the basic rate), https://www.legislation.gov.uk/ukpga/2007/3/section/414 - ITA 2007 s58 (adjusted net income), https://www.legislation.gov.uk/ukpga/2007/3/section/58 - Band figures: see payroll.income-tax's README and data.

Files

PathBytes
README.md2,880
impl/python.py4,195
impl/rust.rs5,291
impl/typescript.ts4,270
vectors.json6,723