Functional Weave
Code in Python

insurance.ipt

UK Insurance Premium Tax at the standard or higher rate in force on a date, added to or taken out of a premium.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

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

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

What it does

UK Insurance Premium Tax (IPT) on a premium, at the rate in force on a date. Give it a net premium and it adds the tax; give it a gross (IPT-inclusive) premium with `includesTax` and it takes the tax out. Either way you get net, tax and gross together, plus the rate and the day that rate came in.

## Rates are data

For example

  • insurance_premium_tax(£100.00, false, standard, 2026-09-23, half-up) → net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 standard rate today: 12% on a 100.00 net premium
  • insurance_premium_tax(£112.00, true, standard, 2026-09-23, half-up) → net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01 taking 12% out of 112.00 gives 12.00 of tax, not 13.44
  • insurance_premium_tax(£50.00, false, higher, 2026-09-23, half-up) → net £50.00, tax £10.00, gross £60.00, rate 20%, band higher, valid from 2011-01-04 higher rate today: 20% on a 50.00 travel premium

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 insurance_premium_tax(amount: Money, includes_tax: bool, band: IptBand, on_date: str, mode: RoundingMode) -> IptBreakdown
amountMoneythe premium: net of IPT, or gross when includesTax is true; negative for a return premium
includes_taxbooltrue to take the tax out of a gross premium, false to add it to a net one
bandIptBandstandard, or higher for travel, mechanical and electrical appliance and some vehicle insurance
on_datedatethe date the insurer receives the premium, which fixes the rate
modeRoundingModehow the one rounded figure rounds: the tax when adding, the net when taking out
returnsIptBreakdown

The types it declares, generated into your project

IptBand = Literal["standard", "higher"]

@dataclass(frozen=True)
class IptBreakdown:
    """Net premium, tax and gross together, with the rate used."""

    net: Money
    tax: Money
    gross: Money
    #: 1200 = 12%
    basis_points: int
    band: IptBand
    #: the day that rate came in
    valid_from: str

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

from fune.insurance.ipt import insurance_premium_tax  # insurance.ipt@^1
impl/python.py · 58 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.

import re
from typing import Optional

from .insurance_ipt_data import IPT_RATES, IPT_RATES_HISTORY  ← this capability’s own data, compiled from data/ipt-rates.json into the same file by fune build
from .insurance_ipt_types import IptBand, IptBreakdown
from .math_round_div import RoundingMode, round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune

ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def insurance_premium_tax(
    amount: Money, includes_tax: bool, band: IptBand, on_date: str, mode: RoundingMode
) -> IptBreakdown:
    """Add UK Insurance Premium Tax to a net premium, or take it out of a gross one,
    at the rate in force on the date the insurer receives the premium.

    Only one figure is ever rounded - the tax when adding, the net when taking
    out - and the other is the difference, so net + tax is always the gross.
    Taking 12% off a gross premium is the classic error: the tax inside 112.00
    is 12.00, not 13.44.
    """
    if band not in ("standard", "higher"):
        raise ValueError('unknown IPT band "%s": use standard or higher' % (band,))
    if not isinstance(on_date, str) or not ISO_DATE.match(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    rule = None
    for r in IPT_RATES:
        if r.band != band or on_date < r.valid_from:
            continue
        if r.valid_to is not None and on_date > r.valid_to:
            continue
        rule = r
    if rule is None:
        # A history=current build keeps only the rates in force; answering an
        # older date with today's rate would be exactly the wrong answer.
        if IPT_RATES_HISTORY != "full":
            earliest: Optional[str] = None
            for r in IPT_RATES:
                if r.band == band and (earliest is None or r.valid_from < earliest):
                    earliest = r.valid_from
            if earliest is not None and on_date < earliest:
                raise ValueError(
                    "no IPT rule for %s on %s: this build was installed with history=%s, "
                    "so it only carries rates from %s. Reinstall with history=full for older premiums."
                    % (band, on_date, IPT_RATES_HISTORY, earliest)
                )
        raise ValueError("no IPT rule for %s on %s" % (band, on_date))
    bp = rule.basis_points
    if includes_tax:
        gross = amount
        net = money(round_div(amount.minor * 10000, 10000 + bp, mode), amount.currency)
        tax = money(amount.minor - net.minor, amount.currency)
    else:
        net = amount
        tax = money(round_div(amount.minor * bp, 10000, mode), amount.currency)
        gross = money(amount.minor + tax.minor, amount.currency)
    return IptBreakdown(net=net, tax=tax, gross=gross, basis_points=bp, band=band, valid_from=rule.valid_from)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 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 insurance.ipt
Download for Python insurance.ipt-1.0.0-python.fune · 20,796 bytes sha256 6d2b2656220498d185fe5c415f3a6cf89800065566f378812acb3958794dc822

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

The whole function, every language, is one file too: insurance.ipt-1.0.0.fune, 27,743 bytes, sha256 ce5ea01e297a92e99d3f2387552203466e2ba1b08834819886754165dfe430a7. 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 insurance.ipt

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

# fune: after insurance.ipt

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 math.round-div in insurance.ipt
# fune: replace money.amount in insurance.ipt

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 insurance.ipt --steps.

# fune: step insurance.ipt 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
standard rate today: 12% on a 100.00 net premium £100.00, false, standard, 2026-09-23, half-up → net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01
taking 12% out of 112.00 gives 12.00 of tax, not 13.44 £112.00, true, standard, 2026-09-23, half-up → net £100.00, tax £12.00, gross £112.00, rate 12%, band standard, valid from 2017-06-01
higher rate today: 20% on a 50.00 travel premium £50.00, false, higher, 2026-09-23, half-up → net £50.00, tax £10.00, gross £60.00, rate 20%, band higher, valid from 2011-01-04
first day of 12%: net of 250.00 gross is 223.21 (22321.43 rounds down) £250.00, true, standard, 2017-06-01, half-up → net £223.21, tax £26.79, gross £250.00, rate 12%, band standard, valid from 2017-06-01
last day of 10%: net of 250.00 gross is 227.27 £250.00, true, standard, 2017-05-31, half-up → net £227.27, tax £22.73, gross £250.00, rate 10%, band standard, valid from 2016-10-01
9.5% on 199.99: 18.99905 rounds half-up to 19.00 £199.99, false, standard, 2016-01-01, half-up → net £199.99, tax £19.00, gross £218.99, rate 9.5%, band standard, valid from 2015-11-01
9.5% on 199.99 rounded down is 18.99 £199.99, false, standard, 2016-01-01, down → net £199.99, tax £18.99, gross £218.98, rate 9.5%, band standard, valid from 2015-11-01
higher rate on its last day at 17.5% £80.00, false, higher, 2011-01-03, half-up → net £80.00, tax £14.00, gross £94.00, rate 17.5%, band higher, valid from 1997-04-01
higher rate on the first day of 20% £80.00, false, higher, 2011-01-04, half-up → net £80.00, tax £16.00, gross £96.00, rate 20%, band higher, valid from 2011-01-04
standard 6% from 4 January 2011: 33.33 gives 2.00 (1.9998) £33.33, false, standard, 2011-01-04, half-up → net £33.33, tax £2.00, gross £35.33, rate 6%, band standard, valid from 2011-01-04
Show the other 11 tests
CaseArgumentsExpected
standard 5% on the day before 6% £105.00, true, standard, 2011-01-03, half-up → net £100.00, tax £5.00, gross £105.00, rate 5%, band standard, valid from 1999-07-01
introductory 2.5% in 1995 £100.00, false, standard, 1995-06-01, half-up → net £100.00, tax £2.50, gross £102.50, rate 2.5%, band standard, valid from 1994-10-01
4% in 1998 £100.00, false, standard, 1998-01-01, half-up → net £100.00, tax £4.00, gross £104.00, rate 4%, band standard, valid from 1997-04-01
the first day IPT existed £40.00, false, standard, 1994-10-01, half-up → net £40.00, tax £1.00, gross £41.00, rate 2.5%, band standard, valid from 1994-10-01
a return premium carries its IPT back: -33.33 at 12% is -4.00 of tax -£33.33, false, standard, 2026-09-23, half-up → net -£33.33, tax -£4.00, gross -£37.33, rate 12%, band standard, valid from 2017-06-01
gross 1.00 at 12%: net 0.89, tax 0.11 £1.00, true, standard, 2026-09-23, half-up → net £0.89, tax £0.11, gross £1.00, rate 12%, band standard, valid from 2017-06-01
a zero premium has no tax £0.00, false, higher, 2026-09-23, half-up → net £0.00, tax £0.00, gross £0.00, rate 20%, band higher, valid from 2011-01-04
no higher rate before 1 April 1997 £100.00, false, higher, 1996-01-01, half-up → error: no IPT rule for higher on 1996-01-01
no IPT before 1 October 1994 £100.00, false, standard, 1994-09-30, half-up → error: no IPT rule for standard on 1994-09-30
a malformed date is refused £100.00, false, standard, 23/09/2026, half-up → error: onDate must be an ISO date
an unknown band is refused £100.00, false, reduced, 2026-09-23, half-up → error: unknown IPT band

More from the author

The rates are dated rows in `data/ipt-rates.json`, never constants in the code, and the function takes the date. There are two bands:

- **standard**: most general insurance (motor, home, pet, commercial). - **higher**: 20% on travel insurance, insurance sold with mechanical or electrical appliances, and some vehicle insurance sold through a supplier of the vehicle (Finance Act 1994, s.51A; see Notice IPT1 for the detail). Which band a policy falls in is the caller's decision.

| from | standard | higher | |---|---|---| | 1 October 1994 | 2.5% | - | | 1 April 1997 | 4% | 17.5% | | 1 July 1999 | 5% | 17.5% | | 4 January 2011 | 6% | 20% | | 1 November 2015 | 9.5% | 20% | | 1 October 2016 | 10% | 20% | | 1 June 2017 | 12% | 20% |

A date before 1 October 1994 (or a higher-rate date before 1 April 1997) is an error, not zero.

## Which date

The rate is fixed by the date the insurer *receives* the premium (or, under the special accounting scheme, the date it is written), not the policy start date. Around each rate change there were anti-forestalling rules for premiums received early for cover starting after the change (HMRC IPT07855); applying those is the caller's job, by passing the date the rules say to use.

## Rounding

Only one figure is rounded, with the caller's mode: the tax when adding, the net when taking out (net = gross x 10000 / (10000 + rate)). The other figure is the difference, so net + tax is exactly the gross. IPT is charged on the whole premium the insurer receives, including administration charges and any charge for paying by instalments (HMRC IPT03400), so apply it after those are added.

Negative premiums (return premiums) are allowed and carry their IPT back.

A build installed with `history=current` keeps only the rates in force and refuses an older date with an explanation rather than answering it at today's rate.

## Sources

- HMRC, "Historical Insurance Premium Tax rates" (IPT bulletin), https://www.gov.uk/government/statistics/insurance-premium-tax-ipt-bulletin/historical-insurance-premium-tax-rates--2 (read 2026-09-23): every date and rate in the table above. - HMRC, "Insurance Premium Tax rates", https://www.gov.uk/government/publications/rates-and-allowances-insurance-premium-tax/insurance-premium-tax-rates (read 2026-09-23): the periods from 3 January 2011 onwards, agreeing. - HMRC Internal Manual IPT03400, "Overview and the law: the rate of tax", https://www.gov.uk/hmrc-internal-manuals/insurance-premium-tax/ipt03400 (read 2026-09-23): the full history of both rates, and that tax is due on the gross premium including administration and instalment charges.

Files

PathBytes
README.md2,988
data/ipt-rates.json1,162
impl/python.py2,727
impl/rust.rs3,965
impl/typescript.ts2,722
vectors.json8,677