Functional Weave
Code in Python

education.attendance-rate

Pupil attendance and absence rates in basis points, with the DfE persistent and severe absence flags.

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

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

What it does

A pupil's attendance and absence rates in basis points (9000 is 90.00%), and whether they are persistently or severely absent under the Department for Education's definitions for schools in England.

## The definitions, and why they are data

For example

  • attendance_rate(380, 38, 2025-10-01) → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent false 38 of 380 sessions missed is exactly 10%: persistently absent
  • attendance_rate(380, 37, 2025-10-01) → attendance basis points 90.26%, absence basis points 9.74%, persistently absent false, severely absent false 37 of 380 is 9.74%: not persistently absent
  • attendance_rate(2,501, 250, 2025-10-01) → attendance basis points 90%, absence basis points 10%, persistently absent false, severely absent false 250 of 2,501 rounds to 10.00% but is under 10%: not persistently absent

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 attendance_rate(possible_sessions: int, missed_sessions: int, on_date: str) -> AttendanceRate
possible_sessionsintsessions the pupil could have attended: two a day, morning and afternoon
missed_sessionsintsessions missed for any reason, authorised or not
on_datedatea date in the period measured, which picks the definitions in force
returnsAttendanceRaterates and flags; the flags use the exact fraction, not the rounded rate

The type it declares, generated into your project

@dataclass(frozen=True)
class AttendanceRate:
    """Attendance, absence and the DfE absence flags for one pupil."""

    #: 10000 minus absenceBasisPoints
    attendance_basis_points: int
    #: missed / possible, rounded half-up to a basis point
    absence_basis_points: int
    #: missed 10% or more of possible sessions
    persistently_absent: bool
    #: missed 50% or more; null on a date before that measure was published
    severely_absent: Optional[bool]

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

from fune.education.attendance_rate import attendance_rate  # education.attendance-rate@^1
impl/python.py · 49 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 Optional

from .dates_add_days import parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .education_attendance_rate_data import ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON  ← this capability’s own data, compiled from data/absence-thresholds.json into the same file by fune build
from .education_attendance_rate_types import AttendanceRate
from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def _threshold_on(measure: str, on_date: str) -> Optional[int]:
    for r in ABSENCE_THRESHOLDS:
        if r.measure == measure and on_date >= r.valid_from and (r.valid_to is None or on_date <= r.valid_to):
            return r.basis_points
    return None


def attendance_rate(possible_sessions: int, missed_sessions: int, on_date: str) -> AttendanceRate:
    """Attendance and absence in basis points, and the DfE absence flags.

    The flags compare missed x 10000 with possible x threshold, exactly: a
    rate of 9.996% rounds to 10.00% but is not persistent absence.
    """
    if not _is_int(possible_sessions) or possible_sessions < 1:
        raise ValueError("possibleSessions must be a whole number of 1 or more, received %s" % (possible_sessions,))
    if not _is_int(missed_sessions) or missed_sessions < 0 or missed_sessions > possible_sessions:
        raise ValueError(
            "missedSessions must be a whole number from 0 to possibleSessions (%s), received %s"
            % (possible_sessions, missed_sessions)
        )
    parse_iso_date(on_date)
    persistent = _threshold_on("persistent", on_date)
    if persistent is None:
        if ABSENCE_THRESHOLDS_HISTORY != "full" and ABSENCE_THRESHOLDS_HORIZON is not None and on_date < ABSENCE_THRESHOLDS_HORIZON:
            raise ValueError(
                "no persistent absence definition on %s: this build was installed with history=%s, so it only carries rules from %s. "
                "Reinstall with history=full for earlier years." % (on_date, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON)
            )
        raise ValueError("no persistent absence definition on %s" % (on_date,))
    severe = _threshold_on("severe", on_date)
    absence = round_div(missed_sessions * 10000, possible_sessions, "half-up")
    return AttendanceRate(
        attendance_basis_points=10000 - absence,
        absence_basis_points=absence,
        persistently_absent=missed_sessions * 10000 >= possible_sessions * persistent,
        severely_absent=None if severe is None else missed_sessions * 10000 >= possible_sessions * severe,
    )

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 education.attendance-rate
Download for Python education.attendance-rate-1.0.1-python.fune · 14,202 bytes sha256 cce1ec617175cbabbe75bfed8d9b487e5f2a322d8a0dd7cb89cabd346cac076c

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

The whole function, every language, is one file too: education.attendance-rate-1.0.1.fune, 20,274 bytes, sha256 628cb864d801b56920bd73fdf860b8bf0b0145e5019b166523a736ed57a86b22. 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 education.attendance-rate

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

# fune: after education.attendance-rate

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 education.attendance-rate
# fune: replace math.round-div in education.attendance-rate

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 education.attendance-rate --steps.

# fune: step education.attendance-rate 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
38 of 380 sessions missed is exactly 10%: persistently absent 380, 38, 2025-10-01 → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent false
37 of 380 is 9.74%: not persistently absent 380, 37, 2025-10-01 → attendance basis points 90.26%, absence basis points 9.74%, persistently absent false, severely absent false
250 of 2,501 rounds to 10.00% but is under 10%: not persistently absent 2,501, 250, 2025-10-01 → attendance basis points 90%, absence basis points 10%, persistently absent false, severely absent false
190 of 380 is exactly half: severely absent 380, 190, 2025-10-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent true
189 of 380 is just under half 380, 189, 2025-10-01 → attendance basis points 50.26%, absence basis points 49.74%, persistently absent true, severely absent false
no sessions missed 380, 0, 2025-10-01 → attendance basis points 100%, absence basis points 0%, persistently absent false, severely absent false
every session missed 380, 380, 2025-10-01 → attendance basis points 0%, absence basis points 100%, persistently absent true, severely absent true
a half basis point rounds absence up and attendance is what is left, so they sum to 100% 20,000, 1, 2025-10-01 → attendance basis points 99.99%, absence basis points 0.01%, persistently absent false, severely absent false
a single possible session, attended 1, 0, 2025-10-01 → attendance basis points 100%, absence basis points 0%, persistently absent false, severely absent false
2016/17, before severe absence was published: severe is null 380, 190, 2016-10-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent —
Show the other 9 tests
CaseArgumentsExpected
the last day before severe absence data 380, 190, 2018-08-31 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent —
the first day of 2018/19 380, 190, 2018-09-01 → attendance basis points 50%, absence basis points 50%, persistently absent true, severely absent true
the first day of the 10% definition 380, 38, 2015-09-01 → attendance basis points 90%, absence basis points 10%, persistently absent true, severely absent —
before 2015/16 there is no per-pupil 10% definition 380, 38, 2015-08-31 → error: no persistent absence definition on 2015-08-31
more sessions missed than possible is an error 380, 381, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received 381
negative missed sessions is an error 380, -1, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received -1
half a session is an error 380, 1.5, 2025-10-01 → error: missedSessions must be a whole number from 0 to possibleSessions (380), received 1.5
no possible sessions is an error 0, 0, 2025-10-01 → error: possibleSessions must be a whole number of 1 or more, received 0
an impossible date is an error 380, 38, 2025-02-30 → error: "2025-02-30" is not a real calendar date

More from the author

- **Persistent absence**: missing 10% or more of the pupil's own possible sessions. This is the definition from the 2015/16 academic year. It was introduced in 2005/06 at 20%, and from 2010/11 was about 15% measured against a standard number of sessions rather than the pupil's own, which cannot be expressed as a percentage of one pupil's sessions; a date before 1 September 2015 is therefore refused rather than answered with today's rule. - **Severe absence**: missing 50% or more. The data carries it from the 2018/19 academic year, the first year for which the DfE statistics release reports it; before 1 September 2018 `severelyAbsent` is null.

Both are dated rows in `data/absence-thresholds.json`, and `onDate` picks the row in force.

## Decisions

- **The flags use the exact fraction.** A pupil who missed 250 of 2,501 sessions has an absence rate of 9.996%, which rounds to 1000 basis points, but is not persistently absent: 250 is less than 10% of 2,501. Comparing the rounded rate is the naive mistake; a vector pins it. - **Absence is rounded, attendance is derived.** Absence is rounded half-up to a basis point and attendance is 10000 minus that, so the two always add up to 100%. (Rounding both half-up gives 10001 when each sits on a half.) - Sessions are whatever the school counts: two per day (morning and afternoon) in England. Missed sessions include authorised and unauthorised absence, as the DfE measures do; pass only the sessions you want counted. - No possible sessions is an error: there is no rate to report.

## Sources

- Department for Education, "Pupil absence statistics: methodology" (Explore education statistics): definitions of persistent absence (10% or more of a pupil's own possible sessions, from 2015/16) and severe absence (50% or more), and the history of the persistent absence threshold: https://explore-education-statistics.service.gov.uk/methodology/pupil-absence-statistics-methodology - Department for Education, "Pupil absence in schools in England: 2018/19", the first release reporting severe absence: https://explore-education-statistics.service.gov.uk/find-statistics/pupil-absence-in-schools-in-england/2018-19

## 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

PathBytes
NOTICE188
README.md2,744
data/absence-thresholds.json477
impl/python.py2,516
impl/rust.rs3,537
impl/typescript.ts2,332
vectors.json4,449