Functional Weave
Code in Python

stats.standard-deviation

Population or sample standard deviation of a list of numbers, two-pass, rounded to stated decimals.

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

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

What it does

How spread out a set of numbers is. The caller says which one they mean, because the two differ and both are called "the standard deviation":

- `population` divides the sum of squared deviations by n. Use it when the values are the whole population (every invoice this month). Excel `STDEV.P`, NumPy `std()`. - `sample` divides by n - 1 (Bessel's correction). Use it when the values are a sample standing in for a larger population. Excel `STDEV.S`, R `sd()`, Python `statistics.stdev`. It needs at least two values.

For example

  • standard_deviation(2, 4, 4, 4, 5, 5, 7, 9, population, 6) → 2 the textbook population example is exactly 2
  • standard_deviation(2, 4, 4, 4, 5, 5, 7, 9, sample, 6) → 2.138 the same data as a sample is sqrt(32/7)
  • standard_deviation(0, 1, population, 0) → 1 a population deviation of exactly 0.5 rounds away from zero to 1

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 standard_deviation(values: Sequence[float], kind: DeviationKind, decimals: int) -> float
valuesfloat[]the data, at least one value (two for a sample)
kindDeviationKindpopulation divides by n (Excel STDEV.P); sample divides by n - 1 (Excel STDEV.S)
decimalsint0 to 12; the result is rounded half away from zero, on the exact value of the double, to this many places
returnsfloat

The type it declares, generated into your project

DeviationKind = Literal["population", "sample"]

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

from fune.stats.standard_deviation import standard_deviation  # stats.standard-deviation@^2
impl/python.py · 43 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 math
from typing import Sequence

from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune
from .stats_standard_deviation_types import DeviationKind


def standard_deviation(values: Sequence[float], kind: DeviationKind, decimals: int) -> float:
    """Population or sample standard deviation, two-pass.

    The one-pass "mean of squares minus square of mean" shortcut cancels
    catastrophically on large, close values; two passes do not.
    """
    if isinstance(values, (str, bytes)) or not isinstance(values, (list, tuple)):
        raise TypeError("values must be a list of numbers")
    for v in values:
        if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
            raise TypeError("values must be finite numbers, received %r" % (v,))
    if kind not in ("population", "sample"):
        raise ValueError('unknown standard deviation kind "%s"' % (kind,))
    # Checked up front as well as in round_float (same wording), so a bad
    # decimals is reported before an empty list, as in 1.x.
    if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0 or decimals > 12:
        raise ValueError("decimals must be a whole number from 0 to 12, received %r" % (decimals,))
    n = len(values)
    if n == 0:
        raise ValueError("values must not be empty")
    if kind == "sample" and n < 2:
        raise ValueError("sample standard deviation needs at least 2 values")

    # Plain left-to-right float additions; sum() on ints would be exact and
    # math.fsum compensated, and either would differ from the other languages.
    total = 0.0
    for v in values:
        total = total + float(v)
    mean = total / n
    squares = 0.0
    for v in values:
        d = float(v) - mean
        squares = squares + d * d
    divisor = n - 1 if kind == "sample" else n
    # math.round-float rounds on the exact value of the double: 2.675 gives 2.67.
    return round_float(math.sqrt(squares / divisor), decimals)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, 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.standard-deviation
Download for Python stats.standard-deviation-2.0.0-python.fune · 9,738 bytes sha256 445894b37c86eb7dc132b3c7bb7b76c9f1d6562758b8e8454888e730101086da

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

The whole function, every language, is one file too: stats.standard-deviation-2.0.0.fune, 13,777 bytes, sha256 21e27bf19a71cebfd8a92b40113bd4376f71d00616efdbdcdfc31ca968a95f44. 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.standard-deviation

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

# fune: after stats.standard-deviation

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 math.round-float in stats.standard-deviation

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.standard-deviation --steps.

# fune: step stats.standard-deviation 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
the textbook population example is exactly 2 2, 4, 4, 4, 5, 5, 7, 9, population, 6 → 2
the same data as a sample is sqrt(32/7) 2, 4, 4, 4, 5, 5, 7, 9, sample, 6 → 2.138
a population deviation of exactly 0.5 rounds away from zero to 1 0, 1, population, 0 → 1
sample of 0 and 1 is sqrt(1/2) 0, 1, sample, 4 → 0.707
large close values do not cancel (population sqrt 22.5) 1,000,000,004, 1,000,000,007, 1,000,000,013, 1,000,000,016, population, 6 → 4.743
large close values do not cancel (sample sqrt 30) 1,000,000,004, 1,000,000,007, 1,000,000,013, 1,000,000,016, sample, 6 → 5.477
one value has no population spread 42.5, population, 3 → 0
a constant sample has no spread 3, 3, 3, sample, 3 → 0
negative values: population of -1 and 1 is 1 -1, 1, population, 3 → 1
negative values: sample of -1 and 1 is sqrt 2 -1, 1, sample, 3 → 1.414
Show the other 11 tests
CaseArgumentsExpected
decimal inputs: population of 0.1, 0.2, 0.3 is sqrt(2/300) 0.1, 0.2, 0.3, population, 6 → 0.082
twelve places of sqrt 1.25 1, 2, 3, 4, population, 12 → 1.118
zero places rounds down below a half 1, 2, 3, 4, population, 0 → 1
population deviation of 0 and 5.35 is the double 2.67499999..., so it rounds to 2.67 (1.x gave 2.68) 0, 5.35, population, 2 → 2.67
population deviation of -2.675 and 2.675 is 2.675 as stored, so 2.67 (1.x gave 2.68) -2.675, 2.675, population, 2 → 2.67
1.45 is stored just below the tie, so it rounds to 1.4 at one place (1.x gave 1.5) 1.45, -1.45, population, 1 → 1.4
an empty list is an error , population, 2 → error: values must not be empty
a sample of one value is an error 5, sample, 2 → error: sample standard deviation needs at least 2 values
an unknown kind is an error 1, 2, unbiased, 2 → error: unknown standard deviation kind "unbiased"
negative decimals is an error 1, 2, sample, -1 → error: decimals must be a whole number from 0 to 12
a non-number value is an error 1, x, sample, 2 → error: values must be finite numbers

More from the author

**Algorithm.** Two passes: the mean first, then the sum of squared deviations from it. The one-pass textbook shortcut, mean of squares minus square of the mean, cancels catastrophically when the values are large and close together (four readings near one billion come out as garbage or even a negative variance); the vectors include that case.

**Precision.** The result is rounded to `decimals` places (0 to 12) by `math.round-float`: half away from zero, decided on the exact value of the double. So a deviation of exactly 0.5 rounds to 1 at zero places, where Python's `round()` gives 0, and a deviation of 2.675 (stored as 2.67499999999999982...) rounds to 2.67 at two places.

**Why the three languages agree to the bit.** Sums run left to right, and only IEEE-754 +, -, x, /, square root and floor are used. All of these are correctly rounded by the standard (square root included, unlike sin or exp, whose last bit varies between math libraries), so TypeScript, Python and Rust hold the same double before rounding, and `math.round-float` rounds it the same way in all three.

## Changes in 2.0.0

2.0.0 rounds on the exact value of the double, so a deviation of 2.675 (the population deviation of 0 and 5.35) now gives 2.67. 1.x rounded the scaled product instead (floor of x x 10^decimals, compared with a half), and 2.675 x 100 is exactly 267.5 in floating point, so 1.x said 2.68. The private rounding helper is gone: this version requires `math.round-float ^1.0.0` and rounds with it, so every float capability in the registry rounds alike.

None of the 1.x vectors changed answers (each was recomputed from the exact value of its double). New vectors pin the difference: the population deviation of 0 and 5.35 gives 2.67 (1.x 2.68), of -2.675 and 2.675 gives 2.67 (1.x 2.68), and of 1.45 and -1.45 at one place gives 1.4 (1.x 1.5).

`decimals` is still 0 to 12, and still refused up front with the same message ("decimals must be a whole number from 0 to 12"), before an empty list or a one-value sample is reported, as in 1.x.

Sources: NIST/SEMATECH e-Handbook of Statistical Methods, section 1.3.5.6 "Measures of Scale"; B. P. Welford, "Note on a Method for Calculating Corrected Sums of Squares and Products", Technometrics 4(3), 1962, on why the one-pass formula fails.

Files

PathBytes
README.md2,843
impl/python.py1,972
impl/rust.rs2,186
impl/typescript.ts1,673
vectors.json2,751