Functional Weave
Code in Python

fleet.mileage-allowance

HMRC approved mileage allowance payments for an employee's own vehicle: tax-free amount, taxable excess or relief.

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

Pinned by 23 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 payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

HMRC Approved Mileage Allowance Payments (AMAPs) for business miles an employee drives in their own vehicle: the approved amount (the most the employer can pay tax-free), the taxable excess when the employer pays more, and the Mileage Allowance Relief (MAR) the employee can claim when it pays less. Passenger payments are worked out alongside.

## The 10,000-mile threshold

For example

  • mileage_allowance(car-van, 0, 1,000, 0, £450.00, £0.00, 2025-06-01) → approved £450.00, paid £450.00, taxable excess £0.00, relief £0.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approve… 1,000 car miles in 2025-26 paid at 45p: all approved, nothing taxable
  • mileage_allowance(car-van, 9,500, 1,000, 0, £450.00, £0.00, 2025-06-01) → approved £350.00, paid £450.00, taxable excess £100.00, relief £0.00, miles at first rate 500, miles at second rate 500, first rate pence 45, second rate pence 25, passenger appro… a claim that crosses 10,000 miles splits: 500 at 45p and 500 at 25p, so a flat 45p leaves 100.00 taxable
  • mileage_allowance(car-van, 12,000, 200, 0, £0.00, £0.00, 2025-12-01) → approved £50.00, paid £0.00, taxable excess £0.00, relief £50.00, miles at first rate 0, miles at second rate 200, first rate pence 45, second rate pence 25, passenger approved £0… every mile past 10,000 is at 25p; nothing paid gives relief on all of it

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 mileage_allowance(vehicle: MileageVehicle, previous_miles: int, miles: int, passenger_miles: int, paid: Money, passenger_paid: Money, on_date: str) -> MileageAllowance
vehicleMileageVehiclecar-van (both share one 10,000-mile threshold), motorcycle or bicycle
previous_milesintbusiness miles in this vehicle kind already paid for earlier in the same tax year and employment
milesintbusiness miles in this claim
passenger_milesintbusiness miles times fellow employees carried: two passengers for 30 miles is 60; cars and vans only
paidMoneywhat the employer pays for these miles, excluding passenger payments
passenger_paidMoneywhat the employer pays in passenger payments for these miles
on_datedateany date in the tax year the journeys were made; decides the rates
returnsMileageAllowance

The types it declares, generated into your project

MileageVehicle = Literal["car-van", "motorcycle", "bicycle"]

@dataclass(frozen=True)
class MileageAllowance:
    """The approved (tax-free) amount for a claim and what is taxable or relievable either side of it."""

    #: the approved amount for the miles: HMRC's tax-free ceiling
    approved: Money
    #: as passed
    paid: Money
    #: paid above approved: taxable, reported on P11D or payrolled
    taxable_excess: Money
    #: approved above paid: Mileage Allowance Relief the employee can claim
    relief: Money
    #: miles at the first-10,000 rate (every mile for a flat-rate vehicle)
    miles_at_first_rate: int
    #: miles beyond 10,000 in the tax year
    miles_at_second_rate: int
    first_rate_pence: int
    second_rate_pence: int
    #: 5p a passenger mile
    passenger_approved: Money
    #: passenger payments above the approved amount; there is no relief below it
    passenger_taxable_excess: Money

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

from fune.fleet.mileage_allowance import mileage_allowance  # fleet.mileage-allowance@^1
impl/python.py · 116 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, Tuple

from .fleet_mileage_allowance_data import AMAP_RATES, AMAP_RATES_HISTORY  ← this capability’s own data, compiled from data/amap-rates.json into the same file by fune build
from .fleet_mileage_allowance_types import MileageAllowance, MileageVehicle
from .money_add import subtract_money  ← from money.add ^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"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _rate_on(vehicle: str, on_date: str):
    best = None
    earliest: Optional[str] = None
    for rule in AMAP_RATES:
        if rule.vehicle != vehicle:
            continue
        if earliest is None or rule.valid_from < earliest:
            earliest = rule.valid_from
        if on_date < rule.valid_from:
            continue
        if rule.valid_to is not None and on_date > rule.valid_to:
            continue
        if best is None or rule.valid_from > best.valid_from:
            best = rule
    if best is not None:
        return best
    # A history=current build has dropped the old rows; refuse rather than pay an
    # old claim at today's rate.
    if AMAP_RATES_HISTORY != "full" and earliest is not None and on_date < earliest:
        raise ValueError(
            "no approved mileage rate for %s on %s: this build was installed with history=%s, "
            "so it only carries rates from %s. Reinstall with history=full for older claims."
            % (vehicle, on_date, AMAP_RATES_HISTORY, earliest)
        )
    raise ValueError("no approved mileage rate for %s on %s" % (vehicle, on_date))


def _whole_miles(name: str, value: int) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError("%s must be a whole number of miles, not negative, received %s" % (name, value))


def _gbp(name: str, value: Money) -> None:
    if value.currency != "GBP":
        raise ValueError("%s must be in GBP, received %s" % (name, value.currency))
    if value.minor < 0:
        raise ValueError("%s must not be negative, received %d" % (name, value.minor))


def _excess_and_shortfall(paid: Money, approved: Money) -> Tuple[Money, Money]:
    difference = subtract_money(paid, approved)
    if difference.minor > 0:
        return difference, money(0, "GBP")
    return money(0, "GBP"), money(-difference.minor, "GBP")


def mileage_allowance(
    vehicle: MileageVehicle,
    previous_miles: int,
    miles: int,
    passenger_miles: int,
    paid: Money,
    passenger_paid: Money,
    on_date: str,
) -> MileageAllowance:
    """HMRC approved mileage allowance payments for business miles in an
    employee's own vehicle.

    The 10,000-mile threshold is per tax year, so a claim that crosses it is
    split: previous_miles says where in the year this claim starts. Paying a
    flat 45p (or 55p) on every mile past 10,000 is the usual mistake; the
    excess over 25p is taxable.
    """
    if vehicle not in ("car-van", "motorcycle", "bicycle"):
        raise ValueError('unknown vehicle "%s": use car-van, motorcycle or bicycle' % (vehicle,))
    _whole_miles("previousMiles", previous_miles)
    _whole_miles("miles", miles)
    _whole_miles("passengerMiles", passenger_miles)
    _gbp("paid", paid)
    _gbp("passengerPaid", passenger_paid)
    if not isinstance(on_date, str) or not ISO_DATE.fullmatch(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    if vehicle != "car-van" and (passenger_miles > 0 or passenger_paid.minor > 0):
        raise ValueError("passenger payments apply only to cars and vans")

    rate = _rate_on(vehicle, on_date)
    miles_at_first_rate = miles
    miles_at_second_rate = 0
    if rate.threshold_miles is not None:
        first_left = max(0, rate.threshold_miles - previous_miles)
        miles_at_first_rate = min(miles, first_left)
        miles_at_second_rate = miles - miles_at_first_rate
    approved = money(
        miles_at_first_rate * rate.first_rate_pence + miles_at_second_rate * rate.second_rate_pence, "GBP"
    )
    taxable_excess, relief = _excess_and_shortfall(paid, approved)

    passenger_approved = money(0, "GBP")
    if vehicle == "car-van":
        passenger_approved = money(passenger_miles * _rate_on("passenger", on_date).first_rate_pence, "GBP")
    # There is no relief for passenger payments below 5p, only tax above it.
    passenger_taxable_excess, _ = _excess_and_shortfall(passenger_paid, passenger_approved)

    return MileageAllowance(
        approved=approved,
        paid=paid,
        taxable_excess=taxable_excess,
        relief=relief,
        miles_at_first_rate=miles_at_first_rate,
        miles_at_second_rate=miles_at_second_rate,
        first_rate_pence=rate.first_rate_pence,
        second_rate_pence=rate.second_rate_pence,
        passenger_approved=passenger_approved,
        passenger_taxable_excess=passenger_taxable_excess,
    )

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 fleet.mileage-allowance
Download for Python fleet.mileage-allowance-1.0.2-python.fune · 34,013 bytes sha256 061d24452ffaa51483bc38087ab1a0897d4b06fcd493128bb52469d1a9a398bd

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

The whole function, every language, is one file too: fleet.mileage-allowance-1.0.2.fune, 45,821 bytes, sha256 7c1c278eb23c990fe4d2ec7e7a9ad6310c79813c7d1da2ddad378f54b24ac2dd. 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 fleet.mileage-allowance

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

# fune: after fleet.mileage-allowance

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.add in fleet.mileage-allowance
# fune: replace money.amount in fleet.mileage-allowance

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 fleet.mileage-allowance --steps.

# fune: step fleet.mileage-allowance 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
1,000 car miles in 2025-26 paid at 45p: all approved, nothing taxable car-van, 0, 1,000, 0, £450.00, £0.00, 2025-06-01 → approved £450.00, paid £450.00, taxable excess £0.00, relief £0.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approve…
a claim that crosses 10,000 miles splits: 500 at 45p and 500 at 25p, so a flat 45p leaves 100.00 taxable car-van, 9,500, 1,000, 0, £450.00, £0.00, 2025-06-01 → approved £350.00, paid £450.00, taxable excess £100.00, relief £0.00, miles at first rate 500, miles at second rate 500, first rate pence 45, second rate pence 25, passenger appro…
every mile past 10,000 is at 25p; nothing paid gives relief on all of it car-van, 12,000, 200, 0, £0.00, £0.00, 2025-12-01 → approved £50.00, paid £0.00, taxable excess £0.00, relief £50.00, miles at first rate 0, miles at second rate 200, first rate pence 45, second rate pence 25, passenger approved £0…
a claim ending exactly on the 10,000th mile is all at the first rate car-van, 9,000, 1,000, 0, £400.00, £0.00, 2025-06-01 → approved £450.00, paid £400.00, taxable excess £0.00, relief £50.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approv…
55p from 6 April 2026: an employer still paying 45p leaves 10p a mile of relief car-van, 0, 100, 0, £45.00, £0.00, 2026-04-06 → approved £55.00, paid £45.00, taxable excess £0.00, relief £10.00, miles at first rate 100, miles at second rate 0, first rate pence 55, second rate pence 25, passenger approved £…
5 April 2026 is still the 2025-26 tax year at 45p car-van, 0, 100, 0, £45.00, £0.00, 2026-04-05 → approved £45.00, paid £45.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0…
40p in the last day of 2010-11 car-van, 0, 100, 0, £40.00, £0.00, 2011-04-05 → approved £40.00, paid £40.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 40, second rate pence 25, passenger approved £0…
45p from 6 April 2011 car-van, 0, 100, 0, £40.00, £0.00, 2011-04-06 → approved £45.00, paid £40.00, taxable excess £0.00, relief £5.00, miles at first rate 100, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0…
the first day of the scheme, 6 April 2002, is 40p car-van, 0, 10, 0, £4.00, £0.00, 2002-04-06 → approved £4.00, paid £4.00, taxable excess £0.00, relief £0.00, miles at first rate 10, miles at second rate 0, first rate pence 40, second rate pence 25, passenger approved £0.00…
a motorcycle is 24p a mile with no threshold motorcycle, 20,000, 100, 0, £24.00, £0.00, 2025-06-01 → approved £24.00, paid £24.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 24, second rate pence 24, passenger approved £0…
Show the other 13 tests
CaseArgumentsExpected
a bicycle at 20p, paid 30p a mile: 10p a mile is taxable bicycle, 0, 50, 0, £15.00, £0.00, 2025-06-01 → approved £10.00, paid £15.00, taxable excess £5.00, relief £0.00, miles at first rate 50, miles at second rate 0, first rate pence 20, second rate pence 20, passenger approved £0.…
two passengers for 75 miles are 150 passenger miles at 5p; 1.00 paid leaves 2.50 taxable car-van, 0, 75, 150, £33.75, £10.00, 2025-06-01 → approved £33.75, paid £33.75, taxable excess £0.00, relief £0.00, miles at first rate 75, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £7.…
passenger payments below 5p give no relief car-van, 0, 75, 150, £33.75, £0.00, 2025-06-01 → approved £33.75, paid £33.75, taxable excess £0.00, relief £0.00, miles at first rate 75, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £7.…
no miles is nothing approved car-van, 0, 0, 0, £0.00, £0.00, 2025-06-01 → approved £0.00, paid £0.00, taxable excess £0.00, relief £0.00, miles at first rate 0, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0.00,…
passenger payments on a motorcycle are refused motorcycle, 0, 10, 10, £2.40, £0.00, 2025-06-01 → error: passenger payments apply only to cars and vans
no approved mileage rate before 6 April 2002 car-van, 0, 10, 0, £0.00, £0.00, 2002-04-05 → error: no approved mileage rate for car-van on 2002-04-05
negative miles are refused car-van, 0, -5, 0, £0.00, £0.00, 2025-06-01 → error: miles must be a whole number of miles, not negative
fractional miles are refused car-van, 0, 10.5, 0, £0.00, £0.00, 2025-06-01 → error: miles must be a whole number of miles, not negative
a payment in another currency is refused car-van, 0, 10, 0, €4.50, £0.00, 2025-06-01 → error: paid must be in GBP
an unknown vehicle is refused lorry, 0, 10, 0, £0.00, £0.00, 2025-06-01 → error: unknown vehicle "lorry"
a malformed date is refused car-van, 0, 10, 0, £0.00, £0.00, 6/4/2025 → error: onDate must be an ISO date
a onDate with a trailing newline is refused car-van, 0, 10, 0, £0.00, £0.00, 2026-09-16 → error: onDate must be an ISO date (YYYY-MM-DD), received "2026-09-16 "
a onDate in Arabic-Indic digits is refused car-van, 0, 10, 0, £0.00, £0.00, ٢٠٢٦-٠٩-١٦ → error: onDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦"

More from the author

Cars and vans get the higher rate for the first 10,000 business miles in the tax year (6 April to 5 April) and 25p after that; the threshold is shared by every car and van the employee uses for the same employment. A claim is usually one month of a year, so `previousMiles` says how many car and van business miles were already paid for this tax year, and the claim is split at the threshold. 9,500 miles already claimed plus 1,000 more is 500 at 45p and 500 at 25p (350.00), not 1,000 at 45p: paying 450.00 leaves 100.00 taxable. Motorcycles and bicycles have one flat rate and no threshold.

## Rates

Dated rows in `data/amap-rates.json`, chosen by `onDate` (any date in the tax year the journeys were made):

| vehicle | from | first 10,000 miles | after | |---|---|---|---| | car or van | 2002-04-06 | 40p | 25p | | car or van | 2011-04-06 | 45p | 25p | | car or van | 2026-04-06 | 55p | 25p | | motorcycle | 2002-04-06 | 24p | 24p | | bicycle | 2002-04-06 | 20p | 20p | | passenger | 2002-04-06 | 5p a passenger mile | |

The 55p rate was announced on 21 May 2026 and applies retrospectively from 6 April 2026, so claims already paid at 45p in 2026-27 have 10p a mile of relief (or can be topped up tax-free). AMAPs began on 6 April 2002; an earlier date is an error. The table declares its effective columns, so a `history=current` build refuses dates before its horizon instead of answering them at today's rate.

## Excess and relief

`taxableExcess` is what was paid above the approved amount; the employer reports it on a P11D or payrolls it. `relief` is the approved amount above what was paid, which the employee claims as MAR. Only one of them is ever non-zero. HMRC works both out over the whole tax year, so if a year's claims are calculated one at a time, total them before reporting.

Passenger payments are 5p per mile for each fellow employee carried on business in a car or van: two passengers for 30 miles is 60 passenger miles. Anything paid above that is taxable; there is no relief below it. Passenger payments for a motorcycle or bicycle are refused.

## Edges

Money must be GBP and not negative; miles must be whole and not negative. Amounts are exact pence (miles times a whole-pence rate), so there is no rounding.

## Sources

- HMRC, "Travel — mileage and fuel rates and allowances" (rates from 2011 to 2026 and from 2026 to 2027, passenger 5p): https://www.gov.uk/government/publications/rates-and-allowances-travel-mileage-and-fuel-allowances/travel-mileage-and-fuel-rates-and-allowances - HM Treasury / HMRC, "Increasing mileage rates" (45p to 55p, retrospective from 6 April 2026): https://www.gov.uk/government/publications/increase-to-approved-mileage-allowance-payments-amaps-and-self-employed-simplified-mileage-rates/increasing-mileage-rates - HMRC, "Expenses and benefits: business travel mileage for employees' own vehicles" (rules for tax, MAR, passenger payments): https://www.gov.uk/expenses-and-benefits-business-travel-mileage/rules-for-tax and https://www.gov.uk/expenses-and-benefits-business-travel-mileage/passenger-payments - HMRC Employment Income Manual EIM31240, "statutory mileage rates: kinds of vehicle": https://www.gov.uk/hmrc-internal-manuals/employment-income-manual/eim31240 - The Approved Mileage Allowance Payments (Rates) Regulations 2011, SI 2011/896 (40p to 45p from 6 April 2011): https://www.legislation.gov.uk/uksi/2011/896/note/made - Income Tax (Earnings and Pensions) Act 2003, s230 as enacted (40p/25p, 24p, 20p) and s234 (5p passenger rate): https://www.legislation.gov.uk/ukpga/2003/1/section/230/enacted , https://www.legislation.gov.uk/ukpga/2003/1/section/234 - Finance Act 2001, Schedule 12 (the same rates from 2002-03): https://www.legislation.gov.uk/ukpga/2001/9/schedule/12/part/1/2001-05-11

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

Legislation: Crown copyright and database right.

1.0.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE236
README.md4,611
data/amap-rates.json1,390
impl/python.py4,798
impl/rust.rs6,668
impl/typescript.ts4,654
vectors.json15,678