Skip to main content

No project description provided

Project description

microbenchmark

A minimal Python library for writing and running benchmarks.

microbenchmark gives you simple building blocks — Scenario, ScenarioGroup, and BenchmarkResult — that you can embed directly into your project or call from CI. No separate CLI package to install; .cli() is built in. You write a Python file, call .run() or .cli(), and you are done.

Key features:

  • A Scenario wraps any callable with an optional argument list and runs it n times, collecting per-run timings.
  • The arguments() helper captures both positional and keyword arguments for the benchmarked function.
  • A ScenarioGroup lets you combine scenarios and run them together with a single call.
  • BenchmarkResult holds every individual duration and gives you mean, median, best, worst, and percentile views.
  • Results can be serialized to and restored from JSON.
  • One dependency: printo (from the mutating organization), used for argument and function display in CLI output.

Table of contents


Installation

pip install microbenchmark

Quick start

from microbenchmark import Scenario

def build_list():
    return list(range(1000))

scenario = Scenario(build_list, number=500)  # name auto-derived as 'build_list'
result = scenario.run()

print(len(result.durations))
#> 500
print(result.mean)   # example — actual value depends on your hardware
#> 0.000012
print(result.median)
#> 0.000011
print(result.best)
#> 0.000010
print(result.worst)
#> 0.000018

arguments

The arguments class (lowercase by design) captures positional and keyword arguments for the benchmarked function. Import it directly:

from microbenchmark import arguments

Or use the short alias a — handy when writing compact benchmark scripts:

from microbenchmark import a

Both arguments and a refer to the same class. Create an instance by calling it like a function:

from microbenchmark import arguments

args = arguments(3, 1, 2)
print(args.args)
#> (3, 1, 2)
print(args.kwargs)
#> {}

args_with_kw = arguments(3, 1, 2, key=str)
print(args_with_kw.args)
#> (3, 1, 2)
print(args_with_kw.kwargs)
#> {'key': <class 'str'>}

The a alias is particularly useful when combining many scenarios inline:

from microbenchmark import Scenario, a

scenario = Scenario(sorted, a([3, 1, 2]), name='sort')
result = scenario.run()

arguments has a readable repr:

from microbenchmark import arguments

print(arguments(1, 2, key='value'))
#> arguments(1, 2, key='value')

print(arguments())
#> arguments()

Scenario

A Scenario describes a single benchmark: the function to call, what arguments to pass, and how many times to run it.

Constructor

Scenario(
    function,
    arguments=None,
    *,
    name=None,
    doc='',
    number=1000,
    timer=time.perf_counter,
)
  • function — the callable to benchmark.
  • arguments — an arguments instance that holds the positional and keyword arguments passed to function on every call. None (the default) means the function is called with no arguments. Supports both positional and keyword arguments.
  • name — a short label for this scenario. If omitted, the name is derived automatically from function.__name__. For lambdas, the derived name will be '<lambda>'.
  • doc — an optional longer description.
  • number — how many times to call function per run. Must be at least 1; passing 0 or a negative value raises ValueError.
  • timer — a zero-argument callable that returns the current time as a float. Defaults to time.perf_counter. Supply a custom clock to get deterministic measurements in tests:
from microbenchmark import Scenario

tick = [0.0]
def fake_timer():
    tick[0] += 0.001
    return tick[0]

scenario = Scenario(lambda: None, name='noop', number=5, timer=fake_timer)
result = scenario.run()
print(result.mean)
#> 0.001
from microbenchmark import Scenario, arguments

scenario = Scenario(
    sorted,
    arguments([3, 1, 2]),
    name='sort_three_items',
    doc='Sort a list of three integers.',
    number=10000,
)
print(scenario.name)
#> sort_three_items
print(scenario.doc)
#> Sort a list of three integers.
print(scenario.number)
#> 10000

When name is omitted, it is derived from the function:

from microbenchmark import Scenario

def my_function():
    return list(range(100))

scenario = Scenario(my_function)
print(scenario.name)
#> my_function

For keyword arguments, pass them through arguments:

from microbenchmark import Scenario, arguments

scenario = Scenario(
    sorted,
    arguments([3, 1, 2], key=lambda x: -x),
    name='sort_descending',
)
result = scenario.run()

For functions that take multiple positional arguments:

from microbenchmark import Scenario, arguments

scenario = Scenario(pow, arguments(2, 10), name='power')
result = scenario.run()
print(result.mean)
#> 0.000001  # example — very fast operation

run(warmup=0)

Runs the benchmark and returns a BenchmarkResult.

The optional warmup argument specifies how many calls to make before timing begins. Warm-up calls execute the function but are not timed and their results are discarded. Warmup is useful when your function has one-time initialization costs — cache warming, lazy imports, JIT compilation — that you do not want to measure. Without warmup, the first few runs may be outliers that skew the mean.

from microbenchmark import Scenario

scenario = Scenario(lambda: list(range(100)), name='build', number=1000)
result = scenario.run(warmup=100)
print(len(result.durations))
#> 1000

cli()

Turns the scenario into a small command-line program. Call scenario.cli() as the entry point of a script and it will parse sys.argv, run the benchmark, and print the result to stdout.

Supported arguments:

  • --number N — override the scenario's number for this run.
  • --max-mean THRESHOLD — exit with code 1 if the mean time (in seconds) exceeds THRESHOLD. Useful in CI.
  • --help — print usage information and exit.

Output format:

benchmark: <name>
call:      <function>(<arguments>)
doc:       <doc>
runs:      <number>
mean:      <mean>s
median:    <median>s
best:      <best>s
worst:     <worst>s
p95 mean:  <p95.mean>s
p99 mean:  <p99.mean>s

The doc: line is omitted when doc is empty. The call: line shows the function name and its arguments. Times are in seconds. Labels are padded to the same width for alignment.

If --max-mean is supplied and the actual mean exceeds the threshold, the output is printed in full and then a failure line is added before the process exits with code 1:

FAIL: mean <actual>s exceeds --max-mean <threshold>s
# benchmark.py
from microbenchmark import Scenario

def build_list():
    return list(range(1000))

scenario = Scenario(build_list, doc='Build a list of 1000 integers.', number=500)

if __name__ == '__main__':
    scenario.cli()
$ python benchmark.py
benchmark: build_list
call:      build_list()
doc:       Build a list of 1000 integers.
runs:      500
mean:      0.000012s
median:    0.000011s
best:      0.000010s
worst:     0.000018s
p95 mean:  0.000011s
p99 mean:  0.000012s

The doc: line is omitted when doc is empty. Use --number to override the run count for this invocation. Use --max-mean to set a CI threshold:

$ python benchmark.py --max-mean 0.000001
benchmark: build_list
call:      build_list()
doc:       Build a list of 1000 integers.
runs:      500
mean:      0.000012s
median:    0.000011s
best:      0.000010s
worst:     0.000018s
p95 mean:  0.000011s
p99 mean:  0.000012s
FAIL: mean 0.000012s exceeds --max-mean 0.000001s
$ echo $?
1

ScenarioGroup

A ScenarioGroup holds a flat collection of scenarios and lets you run them together.

Creating a group

There are four ways to create a group.

Direct construction — pass any number of scenarios to the constructor. Passing no scenarios creates an empty group:

from microbenchmark import Scenario, ScenarioGroup

s1 = Scenario(lambda: None, name='s1')
s2 = Scenario(lambda: None, name='s2')

group = ScenarioGroup(s1, s2)
empty = ScenarioGroup()
print(len(empty.run()))
#> 0

The + operator between two scenarios produces a ScenarioGroup:

from microbenchmark import Scenario

s1 = Scenario(lambda: None, name='s1')
s2 = Scenario(lambda: None, name='s2')
group = s1 + s2
print(type(group).__name__)
#> ScenarioGroup

Adding a scenario to an existing group, or vice versa — the result is always a new flat group with no nesting:

from microbenchmark import Scenario, ScenarioGroup

s1 = Scenario(lambda: None, name='s1')
s2 = Scenario(lambda: None, name='s2')
s3 = Scenario(lambda: None, name='s3')
group = ScenarioGroup(s1, s2)
extended = group + s3     # ScenarioGroup + Scenario
also_ok  = s3 + group     # Scenario + ScenarioGroup
print(len(extended.run()))
#> 3

Adding two groups together produces a single flat group:

from microbenchmark import Scenario, ScenarioGroup

s1 = Scenario(lambda: None, name='s1')
s2 = Scenario(lambda: None, name='s2')
s3 = Scenario(lambda: None, name='s3')
g1 = ScenarioGroup(s1)
g2 = ScenarioGroup(s2, s3)
combined = g1 + g2
print(len(combined.run()))
#> 3

run(warmup=0)

Runs every scenario in order and returns a list of BenchmarkResult objects. The order of results matches the order the scenarios were added. The warmup argument is forwarded to each scenario individually.

from microbenchmark import Scenario, ScenarioGroup

s1 = Scenario(lambda: None, name='s1')
s2 = Scenario(lambda: None, name='s2')
group = ScenarioGroup(s1, s2)
results = group.run(warmup=50)
for result in results:
    print(result.scenario.name)
#> s1
#> s2

cli()

Runs all scenarios and prints their results to stdout. Each scenario block follows the same format as Scenario.cli(), and blocks are separated by a --- line. The separator appears only between blocks, not after the last one.

Supported arguments:

  • --number N — passed to every scenario.
  • --max-mean THRESHOLD — exits with code 1 if any scenario's mean exceeds the threshold.
  • --help — print usage information and exit.
# benchmarks.py
from microbenchmark import Scenario, ScenarioGroup

s1 = Scenario(lambda: list(range(100)), name='range_100')
s2 = Scenario(lambda: list(range(1000)), name='range_1000')

group = s1 + s2

if __name__ == '__main__':
    group.cli()
$ python benchmarks.py
benchmark: range_100
call:      range_100()
runs:      1000
mean:      0.000003s
median:    0.000003s
best:      0.000002s
worst:     0.000005s
p95 mean:  0.000003s
p99 mean:  0.000003s
---
benchmark: range_1000
call:      range_1000()
runs:      1000
mean:      0.000012s
median:    0.000011s
best:      0.000010s
worst:     0.000018s
p95 mean:  0.000011s
p99 mean:  0.000012s

BenchmarkResult

BenchmarkResult is a dataclass that holds the outcome of a single benchmark run.

Fields

  • scenario: Scenario | None — the Scenario that produced this result, or None if the result was restored from JSON.
  • durations: tuple[float, ...] — per-call timings in seconds, one entry per call, in the order they were measured.
  • mean: float — arithmetic mean of durations, computed with math.fsum to minimize floating-point error. Computed automatically from durations.
  • median: float — median of durations. Computed lazily on first access and cached for the lifetime of the result object.
  • best: float — the shortest individual timing. Computed automatically.
  • worst: float — the longest individual timing. Computed automatically.
  • is_primary: boolTrue for results returned directly by run(), False for results derived via percentile(). Preserved during JSON round-trips.

The mean, best, and worst fields are read-only computed values; they are not accepted as constructor arguments. The median, p95, and p99 properties are cached lazily.

from microbenchmark import Scenario

result = Scenario(lambda: None, name='noop', number=100).run()
print(len(result.durations))
#> 100
print(result.is_primary)
#> True
print(isinstance(result.median, float))
#> True

percentile(p)

Returns a new BenchmarkResult containing only the ceil(len(durations) * p / 100) fastest timings, sorted by duration ascending. The returned result has is_primary=False. p must be in the range (0, 100]; passing 0 or a value above 100 raises ValueError.

Percentiles help you focus on the typical case by trimming outliers. If your benchmark includes occasional GC pauses or scheduling jitter, the p95 or p99 view shows what most calls actually experience. is_primary=False marks results that are derived from raw data rather than measured directly; this distinction is preserved during JSON round-trips.

from microbenchmark import Scenario

result = Scenario(lambda: None, name='noop', number=100).run()
trimmed = result.percentile(95)
print(trimmed.is_primary)
#> False
print(len(trimmed.durations))
#> 95

You can call percentile() on a derived result too:

from microbenchmark import Scenario

result = Scenario(lambda: None, name='noop', number=100).run()
print(len(result.percentile(90).percentile(50).durations))
#> 45

p95 and p99

Convenient cached properties that return percentile(95) and percentile(99) respectively. The value is computed once and cached for the lifetime of the result object.

from microbenchmark import Scenario

result = Scenario(lambda: None, name='noop', number=100).run()
print(len(result.p95.durations))
#> 95
print(result.p95.is_primary)
#> False
print(result.p95 is result.p95)   # cached — same object returned each time
#> True

to_json() and from_json()

to_json() serializes the result to a JSON string. It stores durations, is_primary, and the scenario's name, doc, and number.

from_json() is a class method that restores a BenchmarkResult from a JSON string produced by to_json(). Because the original callable cannot be serialized, the restored result has scenario=None. The mean, best, worst, and median fields are recomputed from durations on restoration.

from microbenchmark import Scenario, BenchmarkResult

result = Scenario(lambda: None, name='noop', number=100).run()

json_str = result.to_json()
restored = BenchmarkResult.from_json(json_str)

print(restored.scenario)
#> None
print(restored.mean == result.mean)
#> True
print(restored.durations == result.durations)
#> True
print(restored.is_primary == result.is_primary)
#> True
print(restored.median == result.median)
#> True

Comparison with alternatives

Feature microbenchmark timeit (stdlib) pytest-benchmark
Per-call timings yes via repeat(number=1) yes
Percentile views yes no yes
Median yes no yes
JSON serialization yes no yes
Inject custom timer yes yes no
Warmup support yes no yes (calibration)
CI integration (--max-mean) yes no via configuration
Keyword arguments yes yes yes
+ operator for grouping yes no no
External dependencies one (printo) none several
Embeddable in your own code yes yes pytest plugin required

timeit from the standard library is great for interactive exploration, but it gives only a single aggregate number per call — you can get a list by using repeat(number=1), though the interface is not designed around it. pytest-benchmark is powerful and well-integrated into the pytest ecosystem, but it is tightly coupled to the test runner and brings its own dependencies. microbenchmark sits between the two: richer than timeit, lighter and more portable than pytest-benchmark, and not tied to any test framework.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

microbenchmark-0.0.2.tar.gz (15.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

microbenchmark-0.0.2-py3-none-any.whl (12.3 kB view details)

Uploaded Python 3

File details

Details for the file microbenchmark-0.0.2.tar.gz.

File metadata

  • Download URL: microbenchmark-0.0.2.tar.gz
  • Upload date:
  • Size: 15.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for microbenchmark-0.0.2.tar.gz
Algorithm Hash digest
SHA256 93d9b8ce52090c2f4ab29dbe44f2f2f3fad4583440b8c81d333e9ef31ab75c49
MD5 487e1c55a556977ebfdb17cdd9bc2fc1
BLAKE2b-256 890b8a394ef259ac1ec67ad1463c7908fbb69f2a0624e3ea2cbfab5617f0f26e

See more details on using hashes here.

Provenance

The following attestation bundles were made for microbenchmark-0.0.2.tar.gz:

Publisher: release.yml on mutating/microbenchmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file microbenchmark-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: microbenchmark-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 12.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for microbenchmark-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f65a08a9c71bb1b8690db2b07d534c6b3cd4befbac6126ff0a17b7ad1445078b
MD5 8ec5e43b1c3b18c9b0d227b5fdbde9a1
BLAKE2b-256 e2332bfa14db2b8d08760b8638fe6e216603671d847f420566d2f0bcf5759f52

See more details on using hashes here.

Provenance

The following attestation bundles were made for microbenchmark-0.0.2-py3-none-any.whl:

Publisher: release.yml on mutating/microbenchmark

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page