What you will build
Two dashboards can disagree about p95 even when they receive identical observations. A percentile needs a calculation convention as well as a percentage. Use the standard-library inclusive interpolation method and make that choice part of a small reporting contract.
The fixture contains five durations in milliseconds: 10, 20, 30, 40 and 100. Its p95 is 88 milliseconds, an interpolated value rather than an observed event. This is useful for deterministic batch reports; it is not a streaming sketch or evidence of a service-wide guarantee from a tiny sample.
Prerequisites
Python 3.12 or a later supported Python 3 release. No package installation is needed. Inputs are small lists of nonnegative durations measured in a consistent unit. Save as percentile.py.
from math import isfinite, isclose
from statistics import quantiles
def p95(values):
if len(values) < 2:
raise ValueError('at least two observations required')
if any(type(x) not in (int, float) for x in values):
raise ValueError('numeric observations required')
if any(not isfinite(x) or x < 0 for x in values):
raise ValueError('finite nonnegative durations required')
return quantiles(values, n=100, method='inclusive')[94]
values = [10, 20, 30, 40, 100]
assert isclose(p95(values), 88)
assert p95([7, 7, 7]) == 7
assert p95(list(reversed(values))) == p95(values)
assert p95([0, 100]) == 95
for bad in [[], [1], [1, float('nan')], [1, float('inf')], [1, -1], [1, True]]:
try:
p95(bad)
except ValueError:
pass
else:
raise AssertionError('invalid fixture accepted')
print(f'inclusive_p95={p95(values):.1f}ms')
print('fixtures passed')
Run and expected output
python3 percentile.pyinclusive_p95=88.0ms
fixtures passedHow it works
Python statistics.quantiles divides data into intervals. Inclusive mode places the observed minimum and maximum at the interpolation endpoints. With one hundred intervals, the returned list contains 99 internal cut points; index 94 selects the 95th cut point.
For the sorted five-value fixture, the inclusive position is 0.95 times four, or 3.8 using zero-based indexing. Interpolating 80% of the way from the fourth value, 40, to the fifth, 100, produces 88. This hand calculation provides an independent assertion rather than merely calling the same function twice.
The wrapper rejects too few observations before calling quantiles. It also rejects strings, booleans, negative durations, NaN and infinity. Exact type checks keep booleans out despite their integer ancestry. Remaining tests verify identical values, reversed input order and a two-point interpolation boundary.
Edge cases and scope
This contract requires at least two values even if a future runtime permits other small-sample behavior. A missing percentile differs from a measured zero; callers should report an unavailable value with the observation count when the minimum size is not met. Do not replace missing data with zero.
Inclusive interpolation is one convention, not a universal latency standard. A backend may use nearest-rank selection, histogram buckets or an approximate mergeable sketch. Record method, unit, window and sample count before comparing numbers. Changes to any of these can move a reported percentile without a performance change.
Do not average p95 values from separate servers to obtain pooled p95. For exact batch pooling, combine underlying observations under the same contract. For large-scale telemetry, choose an appropriate aggregation design and validate its approximation separately. This example holds the full list in memory.
Troubleshooting
If another tool disagrees, inspect its interpolation method before adjusting code. If a result becomes NaN, check whether validation was bypassed. If a duration fails validation, confirm types and units at ingestion rather than silently coercing strings. Run without -O so fixture assertions remain active.
Primary references
Continue with data-science tutorials and all Java Samples categories.