legal.statutory-interest Unreviewed
Statutory interest on a judgment debt (8% simple, daily) or a late commercial debt, England and Wales.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 19 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 solicitor 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 legal figures from published rules. It is a software component for developers, not 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 solicitor review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Interest a creditor is entitled to by statute, in England and Wales, on one of three bases:
- `judgment-high-court`: a High Court judgment debt carries simple interest at the rate under section 17 of the Judgments Act 1838, 8% a year since 1 April 1993 (Judgment Debts (Rate of Interest) Order 1993, SI 1993/564), from the date of judgment until payment. - `judgment-county-court`: a County Court judgment carries interest at the same rate (County Courts (Interest on Judgment Debts) Order 1991, SI 1991/1184, art. 5) only if it is a "relevant judgment", one for **not less than GBP 5,000** (art. 1(2)), and from the date it was given (art. 2(1)). A smaller judgment returns zero interest and a rate of 0, which is the right answer, not a failure. The threshold is data. - `late-payment`: statutory interest and the fixed sum under the Late Payment of Commercial Debts (Interest) Act 1998. This is not re-implemented here: it is finance.late-payment-interest, whose interest, rate and fixed sum are passed straight through, with `fromDate` as the due date. Its rate is fixed when interest starts (8% over Bank Rate on the previous 30 June or 31 December), so `rateBasisPoints` is that rate.
For example
statutory_interest(£10,000.00, 2025-01-01, 2025-12-31, judgment-high-court)→ days 364, rate basis points 8%, daily interest £2.19, interest £797.81, compensation £0.00, total £797.81 GBP 10,000 High Court judgment, paid 364 days later: 797.81 at 2.19 a daystatutory_interest(£10,000.00, 2025-01-01, 2026-01-01, judgment-high-court)→ days 365, rate basis points 8%, daily interest £2.19, interest £800.00, compensation £0.00, total £800.00 a whole non-leap year is exactly 8%statutory_interest(£10,000.00, 2024-01-01, 2025-01-01, judgment-high-court)→ days 366, rate basis points 8%, daily interest £2.19, interest £802.19, compensation £0.00, total £802.19 a year spanning 29 February is 366 days at 1/365: 802.19, not 800.00
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 statutory_interest(amount: Money, from_date: str, to_date: str, basis: InterestBasis) -> StatutoryInterest
| amount | Money | the judgment debt, or for late-payment the qualifying debt, in GBP |
| from_date | date | the date of judgment; for late-payment the due date (the relevant day) |
| to_date | date | the date of payment, or the date interest is claimed to |
| basis | InterestBasis | judgment-high-court, judgment-county-court, or late-payment (finance.late-payment-interest) |
| returns | StatutoryInterest |
The types it declares, generated into your project
InterestBasis = Literal["judgment-high-court", "judgment-county-court", "late-payment"]
@dataclass(frozen=True)
class StatutoryInterest:
"""Interest to a date, and the daily rate to quote after it."""
#: days interest runs for: toDate less fromDate
days: int
#: the yearly rate in force on toDate; 0 when no interest runs
rate_basis_points: int
#: one day's interest at that rate, rounded half-up: the "daily rate" a claim form asks for
daily_interest: Money
#: simple interest for the whole period, 1/365 of a year's per day, rounded half-up once
interest: Money
#: late-payment only: the fixed sum; zero for a judgment
compensation: Money
#: interest plus compensation
total: Money
Your code names it in one line, in the file that uses it
from fune.legal.statutory_interest import statutory_interest # legal.statutory-interest@^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 Optional, Sequence
from .dates_add_days import add_days, days_between ← from dates.add-days ^1.0.0 · built alongside by fune
from .finance_late_payment_interest import late_payment_interest ← from finance.late-payment-interest ^1.0.0 · built alongside by fune
from .legal_statutory_interest_data import (
COUNTY_COURT_THRESHOLDS,
COUNTY_COURT_THRESHOLDS_HISTORY,
JUDGMENT_RATES,
JUDGMENT_RATES_HISTORY,
)
from .legal_statutory_interest_types import InterestBasis, StatutoryInterest
from .math_round_div import 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
_YEAR = 10000 * 365
_MAX_EXACT = 2**53 - 1
def _horizon(history: str, froms: Sequence[str], on_date: str, what: str) -> None:
# A history=current build keeps only rows still in force; answering an older
# date with today's rule is the failure dated data exists to prevent.
if history == "full" or len(froms) == 0:
return
earliest = min(froms)
if on_date < earliest:
raise ValueError(
"no %s on %s: this build was installed with history=%s, so it only carries rules from %s. "
"Reinstall with history=full for older judgments." % (what, on_date, history, earliest)
)
def _in_force(on_date: str, valid_from: str, valid_to: Optional[str]) -> bool:
return on_date >= valid_from and (valid_to is None or on_date <= valid_to)
def _rate_on(on_date: str) -> int:
for row in JUDGMENT_RATES:
if _in_force(on_date, row.valid_from, row.valid_to):
return row.basis_points
_horizon(JUDGMENT_RATES_HISTORY, [r.valid_from for r in JUDGMENT_RATES], on_date, "judgment interest rate")
raise ValueError("no judgment interest rate in force on %s" % (on_date,))
def _checked(a: int, b: int, c: int) -> int:
if a * b * c > _MAX_EXACT:
raise ValueError("the interest calculation exceeds the exact integer range")
return a * b * c
def statutory_interest(amount: Money, from_date: str, to_date: str, basis: InterestBasis) -> StatutoryInterest:
"""Statutory interest on a judgment debt or a late commercial debt."""
if amount.currency != "GBP":
raise ValueError("statutory interest is defined for GBP debts, received %s" % (amount.currency,))
if amount.minor <= 0:
raise ValueError("amount must be greater than zero")
zero = money(0, "GBP")
if basis == "late-payment":
claim = late_payment_interest(amount, from_date, to_date)
rate = claim.interest_rate_basis_points or 0
return StatutoryInterest(
days=claim.days_late,
rate_basis_points=rate,
daily_interest=money(round_div(amount.minor * rate, _YEAR, "half-up"), "GBP"),
interest=claim.interest,
compensation=claim.compensation,
total=claim.total,
)
if basis not in ("judgment-high-court", "judgment-county-court"):
raise ValueError(
'unknown interest basis "%s": expected judgment-high-court, judgment-county-court or late-payment' % (basis,)
)
days = days_between(from_date, to_date)
if days < 0:
raise ValueError("toDate %s is before fromDate %s" % (to_date, from_date))
if basis == "judgment-county-court":
threshold = next((r for r in COUNTY_COURT_THRESHOLDS if _in_force(from_date, r.valid_from, r.valid_to)), None)
if threshold is None:
_horizon(
COUNTY_COURT_THRESHOLDS_HISTORY,
[r.valid_from for r in COUNTY_COURT_THRESHOLDS],
from_date,
"County Court judgment interest rule",
)
raise ValueError("no County Court judgment interest rule in force on %s" % (from_date,))
if amount.minor < threshold.min_judgment_minor:
# Not a "relevant judgment": no statutory interest at all.
return StatutoryInterest(days=days, rate_basis_points=0, daily_interest=zero, interest=zero, compensation=zero, total=zero)
# Each day from the day after judgment to the payment date at the rate in force on it.
numerator = 0
covered = 0
first = add_days(from_date, 1)
for row in JUDGMENT_RATES:
if days == 0:
break
start = first if first > row.valid_from else row.valid_from
end = to_date if row.valid_to is None or to_date < row.valid_to else row.valid_to
if start > end:
continue
segment = days_between(start, end) + 1
covered += segment
numerator += _checked(amount.minor, row.basis_points, segment)
if covered != days:
_rate_on(first)
raise ValueError("no judgment interest rate for every day from %s to %s" % (first, to_date))
if numerator > _MAX_EXACT:
raise ValueError("the interest calculation exceeds the exact integer range")
rate = _rate_on(to_date)
interest = money(round_div(numerator, _YEAR, "half-up"), "GBP")
return StatutoryInterest(
days=days,
rate_basis_points=rate,
daily_interest=money(round_div(amount.minor * rate, _YEAR, "half-up"), "GBP"),
interest=interest,
compensation=zero,
total=interest,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 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 legal.statutory-interest
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./legal.statutory-interest-1.0.1-python.fune, or fetch it from a terminal with fune pull legal.statutory-interest@1.0.1:python.
The whole function, every language, is one file too: legal.statutory-interest-1.0.1.fune, 36,256 bytes, sha256 b13808f9559d1b655dfc3ff876d6d8d5e957c3492525c100c6e3a8a8ef97ec9b. 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 legal.statutory-interest
after — your function gets the result and the arguments, and returns the final result.
# fune: after legal.statutory-interest
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 dates.add-days in legal.statutory-interest
# fune: replace finance.late-payment-interest in legal.statutory-interest
# fune: replace math.round-div in legal.statutory-interest
# fune: replace money.amount in legal.statutory-interest
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 legal.statutory-interest --steps.
# fune: step legal.statutory-interest 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 | |
|---|---|---|---|
| GBP 10,000 High Court judgment, paid 364 days later: 797.81 at 2.19 a day | £10,000.00, 2025-01-01, 2025-12-31, judgment-high-court | → | days 364, rate basis points 8%, daily interest £2.19, interest £797.81, compensation £0.00, total £797.81 |
| a whole non-leap year is exactly 8% | £10,000.00, 2025-01-01, 2026-01-01, judgment-high-court | → | days 365, rate basis points 8%, daily interest £2.19, interest £800.00, compensation £0.00, total £800.00 |
| a year spanning 29 February is 366 days at 1/365: 802.19, not 800.00 | £10,000.00, 2024-01-01, 2025-01-01, judgment-high-court | → | days 366, rate basis points 8%, daily interest £2.19, interest £802.19, compensation £0.00, total £802.19 |
| paid on the day of judgment: no interest, but the daily rate is still quoted | £12,345.67, 2026-03-01, 2026-03-01, judgment-high-court | → | days 0, rate basis points 8%, daily interest £2.71, interest £0.00, compensation £0.00, total £0.00 |
| GBP 1 for one day rounds to nothing | £1.00, 2026-03-01, 2026-03-02, judgment-high-court | → | days 1, rate basis points 8%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00 |
| 30 days on GBP 25,000: 164.38 | £25,000.00, 2026-03-01, 2026-03-31, judgment-high-court | → | days 30, rate basis points 8%, daily interest £5.48, interest £164.38, compensation £0.00, total £164.38 |
| rounded once over the period, not per day: 100 days of 1.10 a day on GBP 5,000.00 is 109.59 | £5,000.00, 2026-01-01, 2026-04-11, judgment-county-court | → | days 100, rate basis points 8%, daily interest £1.10, interest £109.59, compensation £0.00, total £109.59 |
| a County Court judgment of GBP 4,999.99 carries no statutory interest | £4,999.99, 2026-01-01, 2026-12-31, judgment-county-court | → | days 364, rate basis points 0%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00 |
| a County Court judgment of exactly GBP 5,000 does | £5,000.00, 2026-01-01, 2027-01-01, judgment-county-court | → | days 365, rate basis points 8%, daily interest £1.10, interest £400.00, compensation £0.00, total £400.00 |
| the first day of the 8% rate | £36,500.00, 1993-03-31, 1993-04-01, judgment-high-court | → | days 1, rate basis points 8%, daily interest £8.00, interest £8.00, compensation £0.00, total £8.00 |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| late payment passes through finance.late-payment-interest: 11.75% and the GBP 70 fixed sum | £1,000.00, 2026-01-31, 2026-03-22, late-payment | → | days 50, rate basis points 11.75%, daily interest £0.32, interest £16.10, compensation £70.00, total £86.10 |
| late payment paid on time: nothing, rate 0 | £1,000.00, 2026-01-31, 2026-01-31, late-payment | → | days 0, rate basis points 0%, daily interest £0.00, interest £0.00, compensation £0.00, total £0.00 |
| a judgment before the 8% rate began (1 April 1993) is not carried | £1,000.00, 1993-03-01, 1993-06-01, judgment-high-court | → | error: judgment interest rate |
| a County Court judgment before 1 July 1991 is not carried | £10,000.00, 1991-06-30, 1993-06-01, judgment-county-court | → | error: County Court judgment interest rule |
| paying before judgment is an error | £1,000.00, 2026-03-02, 2026-03-01, judgment-high-court | → | error: is before fromDate |
| a judgment in euros is an error | €1,000.00, 2026-03-01, 2026-03-31, judgment-high-court | → | error: statutory interest is defined for GBP debts |
| a zero judgment is an error | £0.00, 2026-03-01, 2026-03-31, judgment-high-court | → | error: amount must be greater than zero |
| an unknown basis is an error | £1,000.00, 2026-03-01, 2026-03-31, contract | → | error: unknown interest basis |
| an impossible date is an error | £1,000.00, 2026-02-30, 2026-03-31, judgment-high-court | → | error: 2026-02-30 |
More from the author
**Counting.** Interest runs for `toDate - fromDate` days: judgment on 1 March and paid on 31 March is 30 days. It is simple interest, one day's interest being 1/365 of a year's in leap years too, computed exactly over the whole period and rounded half-up to the penny once, so a year that contains 29 February is 366 days' interest, slightly more than 8%. If the judgment rate ever changes, each day uses the rate in force on it; the data has one row today.
`dailyInterest` is one day's interest at the rate in force on `toDate`, rounded half-up: the "daily rate of interest" a request for judgment or a claim form states for interest after the calculation date.
## Not handled (check before relying on it)
- County Court judgments under GBP 5,000 on a debt that carries interest under the Late Payment Act or by contract: whether and at what rate they carry interest after judgment turns on the Order as amended and on the contract, which this capability does not model; use the late-payment basis for the statutory rate before judgment. - Judgments on regulated Consumer Credit Act agreements (no interest under the 1991 Order, art. 2(3)), instalment orders, suspended possession orders, and interest ceasing while enforcement is stayed or an administration order is in force. - Discretionary pre-judgment interest (s.35A Senior Courts Act 1981, s.69 County Courts Act 1984): the court sets the rate; use lending.daily-interest or money.apply-rate with the rate claimed. - Judgments in foreign currency, and rates before 1 April 1993 (the 15% under the 1985 Order is not carried), which are errors.
## Sources
- Judgments Act 1838, s.17: https://www.legislation.gov.uk/ukpga/Will4and1Vict/1-2/110/section/17 - The Judgment Debts (Rate of Interest) Order 1993 (SI 1993/564), art. 2 and explanatory note, in force 1 April 1993: https://www.legislation.gov.uk/uksi/1993/564/made - The County Courts (Interest on Judgment Debts) Order 1991 (SI 1991/1184), arts. 1, 2 and 5, in force 1 July 1991: https://www.legislation.gov.uk/uksi/1991/1184/contents - Late Payment: see finance.late-payment-interest.
Read on 23 September 2026. Both tables declare effective dates, so a `history=current` build keeps only the rules in force and refuses an earlier date instead of answering it with today's rule.
## Before you rely on this
**Not professional advice.** This capability calculates legal figures from published rules. It is a software component for developers, not 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 solicitor 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 solicitor 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/).
Legislation: Crown copyright and database right.
1.0.1 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 237 |
| README.md | 4,905 |
| data/county-court-thresholds.json | 268 |
| data/judgment-rates.json | 232 |
| impl/python.py | 5,068 |
| impl/rust.rs | 6,391 |
| impl/typescript.ts | 4,947 |
| vectors.json | 7,917 |