Functional Weave
Code in Python

stats.mean-median

Mean, median and mode of a list of integers, with the mean and median as exact fractions.

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

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

What it does

Mean, median and mode of a list of integers, in one pass over one sorted copy.

The mean and median are returned as exact fractions in lowest terms (`7/2`, not `3.5` and certainly not `3`, which is what integer division gives). Each also comes as a float, `meanValue` and `medianValue`, computed by one IEEE-754 division of the exact numerator by the exact denominator. That single division is correctly rounded in every language, so TypeScript, Python and Rust return the same double to the last bit; there is no accumulated float error to disagree about, because the sum is an exact integer.

For example

  • mean_median_mode(1, 2, 3, 4) → count 4, sum 10, mean …, mean value 2.5, median …, median value 2.5, modes 1, 2, 3, 4, mode frequency 1 an even count has a fractional mean and median
  • mean_median_mode(3, 4) → count 2, sum 7, mean …, mean value 3.5, median …, median value 3.5, modes 3, 4, mode frequency 1 the mean of 3 and 4 is 7/2, not the 3 integer division gives
  • mean_median_mode(2, 2, 3, 9, 1) → count 5, sum 17, mean …, mean value 3.4, median …, median value 2, modes 2, mode frequency 2 unsorted odd-length input takes the middle of the sorted values

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 mean_median_mode(values: Sequence[int]) -> CentralTendency
valuesint[]whole numbers in any order, at least one; scale decimals first (pence, grams)
returnsCentralTendency

The types it declares, generated into your project

@dataclass(frozen=True)
class Fraction:
    """An exact rational number in lowest terms; the denominator is always positive."""

    numerator: int
    denominator: int

@dataclass(frozen=True)
class CentralTendency:
    """The three averages of one list, exact where they can be."""

    count: int
    sum: int
    mean: Fraction
    #: sum / count, the nearest binary64 to the exact mean
    mean_value: float
    #: denominator 1 or 2
    median: Fraction
    median_value: float
    #: every value with the highest frequency, ascending
    modes: List[int]
    #: 1 means no value repeats, so every value is a mode
    mode_frequency: int

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

from fune.stats.mean_median import mean_median_mode  # stats.mean-median@^1
impl/python.py · 77 lines · open · raw
from math import gcd
from typing import List, Sequence

from .stats_mean_median_types import CentralTendency, Fraction


MAX_SAFE = 9007199254740991


def _fraction(numerator: int, denominator: int) -> Fraction:
    g = gcd(numerator, denominator) or 1
    return Fraction(numerator=numerator // g, denominator=denominator // g)


def mean_median_mode(values: Sequence[int]) -> CentralTendency:
    """Mean, median and mode of a list of integers.

    Mean and median are exact fractions; their float forms come from a single
    correctly rounded division, so every language returns the same double.
    """
    if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)):
        raise TypeError("values must be a list of integers")
    if len(values) == 0:
        raise ValueError("values must not be empty")
    for v in values:
        # bool is an int in Python; every other language rejects it, so do too.
        if isinstance(v, bool) or not isinstance(v, int):
            raise TypeError("values must be integers, received %r" % (v,))
        if abs(v) > MAX_SAFE:
            raise ValueError("values must be safe integers (magnitude at most 9007199254740991), received %d" % v)

    total = sum(values)
    if abs(total) > MAX_SAFE:
        raise ValueError("sum exceeds the safe integer range")
    count = len(values)

    ordered = sorted(values)
    if count % 2 == 1:
        median = Fraction(numerator=ordered[(count - 1) // 2], denominator=1)
    else:
        pair = ordered[count // 2 - 1] + ordered[count // 2]
        if pair % 2 == 0:
            median = Fraction(numerator=pair // 2, denominator=1)
        else:
            if abs(pair) > MAX_SAFE:
                raise ValueError("median exceeds the safe integer range")
            median = Fraction(numerator=pair, denominator=2)

    # Runs of equal values in the sorted copy give frequencies in ascending
    # value order, so the modes come out sorted without a second sort.
    modes: List[int] = []
    mode_frequency = 0
    i = 0
    while i < count:
        j = i
        while j < count and ordered[j] == ordered[i]:
            j += 1
        run = j - i
        if run > mode_frequency:
            mode_frequency = run
            modes = [ordered[i]]
        elif run == mode_frequency:
            modes.append(ordered[i])
        i = j

    # float(a) / float(b) with both exact (|a| <= 2^53) is one correctly
    # rounded IEEE division, the same double TypeScript and Rust produce.
    return CentralTendency(
        count=count,
        sum=total,
        mean=_fraction(total, count),
        mean_value=float(total) / float(count) + 0.0,
        median=median,
        median_value=float(median.numerator) / float(median.denominator) + 0.0,
        modes=modes,
        mode_frequency=mode_frequency,
    )

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 stats.mean-median
Download for Python stats.mean-median-1.0.0-python.fune · 11,656 bytes sha256 1f8257abec5133c919d1b587d720d46eeaf8cfd76b1ce00409a7d82964339222

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

The whole function, every language, is one file too: stats.mean-median-1.0.0.fune, 19,364 bytes, sha256 f90bdb4452a1f9b675c30352a8731b29cf937dc78ea902e7b0d2a54d83d9bdf5. 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 stats.mean-median

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

# fune: after stats.mean-median

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 stats.mean-median --steps.

# fune: step stats.mean-median 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
an even count has a fractional mean and median 1, 2, 3, 4 → count 4, sum 10, mean …, mean value 2.5, median …, median value 2.5, modes 1, 2, 3, 4, mode frequency 1
the mean of 3 and 4 is 7/2, not the 3 integer division gives 3, 4 → count 2, sum 7, mean …, mean value 3.5, median …, median value 3.5, modes 3, 4, mode frequency 1
unsorted odd-length input takes the middle of the sorted values 2, 2, 3, 9, 1 → count 5, sum 17, mean …, mean value 3.4, median …, median value 2, modes 2, mode frequency 2
the median sorts numerically, not as strings (10 is not before 9) 10, 9, 1 → count 3, sum 20, mean …, mean value 6.667, median …, median value 9, modes 1, 9, 10, mode frequency 1
tied modes are all returned, ascending 5, 3, 5, 3, 1 → count 5, sum 17, mean …, mean value 3.4, median …, median value 3, modes 3, 5, mode frequency 2
negative values keep the sign on the numerator -3, -2 → count 2, sum -5, mean …, mean value -2.5, median …, median value -2.5, modes -3, -2, mode frequency 1
a single value is its own mean, median and mode 7 → count 1, sum 7, mean …, mean value 7, median …, median value 7, modes 7, mode frequency 1
a zero mean is 0/1 in lowest terms -4, 4 → count 2, sum 0, mean …, mean value 0, median …, median value 0, modes -4, 4, mode frequency 1
fractions are reduced to lowest terms 2, 4, 6, 8 → count 4, sum 20, mean …, mean value 5, median …, median value 5, modes 2, 4, 6, 8, mode frequency 1
a repeating mean is exact as a fraction 1, 1, 2 → count 3, sum 4, mean …, mean value 1.333, median …, median value 1, modes 1, mode frequency 2
Show the other 6 tests
CaseArgumentsExpected
values at the edge of the safe range 9,007,199,254,740,991, -9,007,199,254,740,991 → count 2, sum 0, mean …, mean value 0, median …, median value 0, modes -9,007,199,254,740,991, 9,007,199,254,740,991, mode frequency 1
an empty list is an error → error: values must not be empty
a fractional value is an error 1, 2.5 → error: values must be integers
a value past 2^53 - 1 is an error 9,007,199,254,740,992 → error: values must be safe integers
a sum past the safe range is an error, not a rounded answer 9,007,199,254,740,991, 1 → error: sum exceeds the safe integer range
an odd median pair past the safe range is an error -9,007,199,254,740,991, 4,503,599,627,370,496, 4,503,599,627,370,497, 4,503,599,627,370,497 → error: median exceeds the safe integer range

More from the author

Inputs are integers on purpose. Averages of money, weights or counts should be taken in their smallest unit (pence, grams), where they are exact; divide at the edge. Every value must be a safe integer (magnitude at most 2^53 - 1, the range all three languages share), and so must the sum. A list whose sum leaves that range is an error, not a silently rounded answer. The same goes for the median of an even-length list when the two middle values add up past it and do not halve evenly.

The median sorts numerically. A naive JavaScript `values.sort()` sorts as strings and puts 10 before 9; the vectors pin that down. The input list is never mutated.

`modes` lists every value that shares the highest frequency, ascending. When no value repeats, `modeFrequency` is 1 and every value is listed: whether that counts as "no mode" is the caller's call, and `modeFrequency` makes it a one-line test.

An empty list is an error: there is no honest mean of nothing.

Files

PathBytes
README.md1,577
impl/python.py2,829
impl/rust.rs4,284
impl/typescript.ts3,117
vectors.json4,002