fleet.emissions
Vehicle greenhouse gas emissions (grams CO2e) from fuel used or distance, using UK government DESNZ conversion factors.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 23 tests, run in TypeScript, Python and Rust.
What it does
Greenhouse gas emissions, in grams of CO2 equivalent, for fuel a fleet burned or distance it drove, using the UK government's conversion factors for company reporting (published by DESNZ, the Department for Energy Security and Net Zero). `fleetEmissions("fuel-diesel", "100", "litres", 2025)` is 257,082 g.
## Which factor
For example
fleet_emissions(fuel-diesel, 100, litres, 2,025)→ co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres 100 litres of pump diesel in 2025 is 257.082 kgfleet_emissions(fuel-petrol, 40.5, litres, 2,025)→ co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres 40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801fleet_emissions(fuel-diesel, 1, litres, 2,023)→ co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres one litre of diesel in the 2023 set
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 fleet_emissions(activity: str, quantity: str, unit: EmissionsUnit, factor_year: int) -> FleetEmissions
| activity | string | a fuel (fuel-diesel, fuel-petrol, ...) or a vehicle class (car-medium-petrol, van-average-diesel, hgv-all-hgvs-laden-average, ...) |
| quantity | string | plain decimal text, not negative: at most 15 significant digits and 9 decimal places |
| unit | EmissionsUnit | litres for a fuel; km or miles for a vehicle class |
| factor_year | int | the DESNZ factor set, 2023 to 2026: normally the year you are reporting |
| returns | FleetEmissions | scope 1 (direct, tailpipe) emissions, rounded half up to a whole gram |
The types it declares, generated into your project
EmissionsUnit = Literal["litres", "km", "miles"]
@dataclass(frozen=True)
class FleetEmissions:
"""The emissions and the factor that produced them, for the audit trail."""
#: quantity times factor, rounded half up to the gram
co2e_grams: int
#: the factor as published, kg CO2e per unit, to 5 decimal places
factor: str
factor_year: int
activity: str
unit: str
Your code names it in one line, in the file that uses it
from fune.fleet.emissions import fleet_emissions # fleet.emissions@^1
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 .fleet_emissions_data import GHG_FACTORS ← this capability’s own data, compiled from data/ghg-factors.json into the same file by fune build
from .fleet_emissions_types import EmissionsUnit, FleetEmissions
DECIMAL = re.compile(r"([0-9]+)(?:\.([0-9]+))?")
MAX_SAFE = 9007199254740991
def _format_factor(factor: int) -> str:
return "%d.%05d" % (factor // 100000, factor % 100000)
def fleet_emissions(activity: str, quantity: str, unit: EmissionsUnit, factor_year: int) -> FleetEmissions:
"""Scope 1 emissions for fuel burned or distance driven, from the UK
government's conversion factors for company reporting.
The quantity is decimal text and the factor an integer in 1e-5 kg, so the
product is exact and rounded once, to the gram. Multiplying floats instead
drifts in the last place and can round a half-gram the wrong way.
"""
if unit not in ("litres", "km", "miles"):
raise ValueError('unknown unit "%s": use litres, km or miles' % (unit,))
if isinstance(factor_year, bool) or not isinstance(factor_year, int):
raise ValueError("factorYear must be a whole year, received %s" % (factor_year,))
match = DECIMAL.fullmatch(quantity) if isinstance(quantity, str) else None
if match is None:
raise ValueError('quantity must be plain decimal text, not negative, received "%s"' % (quantity,))
fraction = match.group(2) or ""
digits = (match.group(1) + fraction).lstrip("0")
if len(digits) > 15 or len(fraction) > 9:
raise ValueError(
'quantity must have at most 15 significant digits and 9 decimal places, received "%s"' % (quantity,)
)
found: Optional[int] = None
year_known = False
activity_known = False
for row in GHG_FACTORS:
if row.factor_year != factor_year:
continue
year_known = True
if row.activity != activity:
continue
activity_known = True
if row.unit == unit:
found = row.factor
if not year_known:
raise ValueError("no conversion factors for %d: this package carries 2023 to 2026" % (factor_year,))
if not activity_known:
raise ValueError('unknown activity "%s" in the %d factors' % (activity, factor_year))
if found is None:
raise ValueError("no conversion factor for %s per %s in %d" % (activity, unit, factor_year))
# grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100
numerator = int(match.group(1) + fraction) * found
denominator = 10 ** len(fraction) * 100
grams = (2 * numerator + denominator) // (2 * denominator)
if grams > MAX_SAFE:
raise ValueError("the result exceeds 2^53 - 1 grams")
return FleetEmissions(
co2e_grams=grams, factor=_format_factor(found), factor_year=factor_year, activity=activity, unit=unit
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, 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.emissions
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./fleet.emissions-1.0.1-python.fune, or fetch it from a terminal with fune pull fleet.emissions@1.0.1:python.
The whole function, every language, is one file too: fleet.emissions-1.0.1.fune, 142,757 bytes, sha256 889b6473407bbb10b8b415ea8a5d9fa9ddc9d09a15beacbd289c885db32cce5c. 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.emissions
after — your function gets the result and the arguments, and returns the final result.
# fune: after fleet.emissions
replace — it requires no other capability, so there is no dependency to replace.
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.emissions --steps.
# fune: step fleet.emissions 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 | |
|---|---|---|---|
| 100 litres of pump diesel in 2025 is 257.082 kg | fuel-diesel, 100, litres, 2,025 | → | co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres |
| 40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801 | fuel-petrol, 40.5, litres, 2,025 | → | co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres |
| one litre of diesel in the 2023 set | fuel-diesel, 1, litres, 2,023 | → | co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres |
| one litre of diesel in the 2024 set | fuel-diesel, 1, litres, 2,024 | → | co2e grams 2,513, factor 2.51279, factor year 2,024, activity fuel-diesel, unit litres |
| one litre of diesel in the 2026 set | fuel-diesel, 1, litres, 2,026 | → | co2e grams 2,584, factor 2.58354, factor year 2,026, activity fuel-diesel, unit litres |
| an exact half gram rounds up: 0.1 litre of 2026 petrol is 207.5 g | fuel-petrol, 0.1, litres, 2,026 | → | co2e grams 208, factor 2.07500, factor year 2,026, activity fuel-petrol, unit litres |
| 1,000 km in an average petrol car, 2025 | car-average-petrol, 1000, km, 2,025 | → | co2e grams 162,720, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km |
| 100 miles uses DESNZ's own per-mile factor | car-average-petrol, 100, miles, 2,025 | → | co2e grams 26,187, factor 0.26187, factor year 2,025, activity car-average-petrol, unit miles |
| the same trip in km (160.9344) agrees to the gram | car-average-petrol, 160.9344, km, 2,025 | → | co2e grams 26,187, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km |
| 250 km in an average laden HGV in 2026: 224,357.5 g rounds up | hgv-all-hgvs-laden-average, 250, km, 2,026 | → | co2e grams 224,358, factor 0.89743, factor year 2,026, activity hgv-all-hgvs-laden-average, unit km |
Show the other 13 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| zero distance is zero | car-average-petrol, 0, km, 2,025 | → | co2e grams 0, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km |
| a nine-decimal quantity is accepted and rounds to nothing | fuel-diesel, 0.000000001, litres, 2,025 | → | co2e grams 0, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres |
| a fuel has no per-km factor | fuel-diesel, 10, km, 2,025 | → | error: no conversion factor for fuel-diesel per km in 2025 |
| a vehicle class has no per-litre factor | car-average-petrol, 10, litres, 2,025 | → | error: no conversion factor for car-average-petrol per litres in 2025 |
| battery electric cars are not carried: their emissions are electricity, not scope 1 | car-average-bev, 10, km, 2,025 | → | error: unknown activity "car-average-bev" in the 2025 factors |
| van PHEV factors start in 2024 | van-average-phev, 10, km, 2,023 | → | error: unknown activity "van-average-phev" in the 2023 factors |
| 2022 factors are not carried | fuel-diesel, 10, litres, 2,022 | → | error: no conversion factors for 2022 |
| a negative quantity is refused | fuel-diesel, -5, litres, 2,025 | → | error: quantity must be plain decimal text, not negative |
| exponent notation is refused | fuel-diesel, 1e3, litres, 2,025 | → | error: quantity must be plain decimal text, not negative |
| a number instead of text is refused | fuel-diesel, 12, litres, 2,025 | → | error: quantity must be plain decimal text, not negative |
| ten decimal places are too many | fuel-diesel, 0.0000000001, litres, 2,025 | → | error: at most 15 significant digits and 9 decimal places |
| an unknown unit is refused | fuel-diesel, 10, gallons, 2,025 | → | error: unknown unit "gallons" |
| a result beyond 2^53 - 1 grams is refused | fuel-diesel, 123456789012345, litres, 2,025 | → | error: the result exceeds 2^53 - 1 grams |
More from the author
Fuel is the better measure when you have it (fuel cards, bunkered fuel): it already reflects how the vehicles were driven. Distance by vehicle class is the fallback. The factors are the **Scope 1** figures, direct tailpipe emissions in kg CO2e (CO2, CH4 and N2O together):
- fuels, per litre: `fuel-diesel` and `fuel-petrol` (UK forecourt fuel with its average biofuel blend), `fuel-diesel-mineral` and `fuel-petrol-mineral` (100% mineral), `fuel-lpg`; - cars by size, per km or mile: `car-{small|medium|large|average}-{diesel|petrol|hybrid|phev|cng|lpg|unknown}` where DESNZ publishes one (there is no small CNG or LPG car); - `motorbike-{small|medium|large|average}`; - vans: `van-{class-1|class-2|class-3|average}-{diesel|petrol}`, and `van-average-{cng|lpg|unknown|phev}` and `van-class-3-phev` (the van PHEV factors appear from 2024); - HGVs (all diesel): `hgv-{rigid-3.5-7.5t|rigid-7.5-17t|rigid-over-17t|all-rigids|artic-3.5-33t|artic-over-33t|all-artics|all-hgvs}-laden-{0|50|100|average}`.
Battery electric vehicles are deliberately absent: their Scope 1 factor is zero, and their emissions belong to the electricity used (Scope 2, DESNZ's "UK electricity for EVs" table), which this package does not carry. A plug-in hybrid's factor likewise covers only the fuel it burns.
Per-mile factors are DESNZ's own, not the per-km factor times 1.609344, so a report matches the published spreadsheet exactly.
## Which year
`factorYear` picks the DESNZ set: 2023, 2024, 2025 or 2026. DESNZ publishes each set in June for reporting periods in that year; use the set for the year you are reporting, and keep it for restatements. The sets are publications, not rules that expire, so the table has no effective columns and ships whole.
In the 2026 set DESNZ split HGVs into refrigerated and non-refrigerated and renamed the averages; the `hgv-*` codes read the non-refrigerated rows for 2026 (earlier years' "HGV (all diesel)" table is the non-refrigerated fleet, refrigerated HGVs having had their own table), so the codes mean the same vehicle in every year.
## Exactness
Each factor is stored as an integer in units of 0.00001 kg (10 mg): DESNZ publishes five decimal places. The 2023 flat file holds the unrounded values behind them (2.5120638845637586 for diesel); they are rounded half up to the five places shown in the published tables (2.51206). The quantity is decimal text, so quantity times factor is exact, and the only rounding is the final one, half up to a whole gram. Quantities are limited to 15 significant digits and 9 decimal places, and a result above 2^53 - 1 grams is an error, so all three languages agree.
## Relationship to energy.*
This package is self-contained. If an `energy.carbon-emissions` capability with the same DESNZ tables appears in the registry, a later version of this one should read its data instead of carrying a second copy.
## Sources
- DESNZ, "Greenhouse gas reporting: conversion factors 2023", flat file (ghg-conversion-factors-2023-flat-file-update.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2023 - DESNZ, "Greenhouse gas reporting: conversion factors 2024", flat file v1.1 (ghg-conversion-factors-2024-FlatFormat_v1_1.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2024 - DESNZ, "Greenhouse gas reporting: conversion factors 2025", flat file (ghg-conversion-factors-2025-flat-format.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2025 - DESNZ, "Greenhouse gas reporting: conversion factors 2026", flat file, revised July 2026 (ghg-conversion-factors-2026-flat-format-revised.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2026
Every row of `data/ghg-factors.json` names the spreadsheet path it came from (Level 1 / Level 2 / Level 3 / column), always the "kg CO2e" row of Scope 1.
## 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 adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 178 |
| README.md | 4,562 |
| data/ghg-factors.json | 106,856 |
| impl/python.py | 2,788 |
| impl/rust.rs | 4,419 |
| impl/typescript.ts | 2,804 |
| vectors.json | 6,107 |