misu is short for "misura", which means measurement (in
Italian). misu is a package for doing calculations with in consistent
units of measurement.
Precompiled wheels are published to PyPI for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x64), covering Python 3.9 and later. There is nothing to compile.
uv add misu # in a uv project
uv pip install misu
pip install misuMost of the time you will probably work with misu interactively, and
it will be most convenient to import the entire namespace:
from misu import *
mass = 100*kg
print(mass >> lb)The symbol kg got imported from the misu package. We redefine
the shift operator to perform inline conversions. The code above
produces:
220.46226218487757
There are many units already defined, and it is easy to add more. Here we convert the same quantity into ounces:
print(mass >> oz)output:
3571.4285714285716
What you see above would be useless on its own. What you really need is to be able to perform consistent calculations with quantities expressed in different, but compatible units:
mass = 10*kg + 20*lb
print(mass)output:
19.07 kg
For addition and subtraction, misu will ensure that only consistent
units can be used. Multiplication and division will produce new units:
distance = 100*metres
time = 9.2*seconds
speed = distance / time
print(speed)output:
10.87 m/s
As before, it is trivially easy to express that quantity in different units of compatible dimensions:
print(speed >> km/hr)output:
39.130434782608695
misu is a package of handling physical quantities with dimensions.
This means performing calculations with all the units being tracked
correctly. It is possible to add kilograms per hour to ounces per
minute, obtain the correct answer, and have that answer be reported in,
say, pounds per week.
misu grew out of a personal need. I have used this code personally
in a (chemical) engineering context for well over a year now (at time of
writing, Feb 2015). Every feature has been added in response to a
personal need.
- Speed optimized.
misuis very fast! Heavy math code in Python is only around 5X slower when expressed withmisuquantities instead of plain floats — see Performance below for measured numbers. This is much faster than other quantities packages for Python. - Implemented as a Rust extension module via PyO3, so the hot paths run as native code.
- When an operation involving incompatible units is attempted, an
EIncompatibleUnitsexception is raised, with a clear explanation message about which units were inconsistent. - Decorators for functions to enforce dimensions
@dimensions(x='Length', y='Mass')
def f(x, y):
return x/y
f(2*m, 3*kg) # Works
f(200*feet, 3*tons) # Works
f(2*joules, 3*kelvin) # raises AssertionError
f(2*m, 3) # raises AssertionError- An operator for easily stripping the units component to obtain a plain numerical value
mass = 100 * kg
mass_lb = mass >> lb
duty = 50 * MW
duty_BTU_hr = duty >> BTU / hr- An enormous amount of redundancy in the naming of various units. This
means that
m,metre,metres,METRE,METRESwill all work. The reason for this is that from my own experience, when working interactively (e.g. in the IPython Notebook) it can be very distracting to incorrectly guess the name for a particular unit, and have to look it up.ft,footandfeetall work,m3meansm**3and so on. - You can specify a reporting unit for a dimension, meaning that you could have all lengths be reported in "feet" by default for example.
- You can specify a reporting format for a particular unit.
Two looping numerical workloads, each timed in plain Python floats and
then with misu quantities (so every operation in the inner loop pays
the unit-tracking cost). The benchmark lives at
scripts/benchmark.py and can be re-run any time:
python scripts/benchmark.pyRepresentative numbers on Python 3.14, best of five runs:
| Workload | float (ms) | misu (ms) | slowdown |
|---|---|---|---|
fall_with_drag — 200k Euler steps, 1-D free-fall with
quadratic drag |
~19 | ~103 | ~5.2x |
orbit_step — 100k 2-D Kepler steps, sqrt-heavy |
~18 | ~91 | ~5.1x |
| Geometric mean | ~5.1x |
The original 5x heuristic was measured back when misu was a Cython
extension; the rewrite to a Rust/PyO3 extension lands in the same
ballpark, presumably because the dominant cost is the Python-call
boundary around each __mul__ / __add__ rather than the
unit-arithmetic itself.
There are several units systems for Python, but the primary motivating
use-case is that misu is written as a Rust extension module and is
by far the fastest* for managing units available in Python.
*Except for ``NumericalUnits``, which is a special case
**I haven't actually checked that this statement is true for all of them yet.
For speed-critical code, the application of unit operations can still be too slow. In these situations it is typical to first cast quantities into numerical values (doubles, say), perform the speed-critical calculations (perhaps call into a C-library), and then re-cast the result back into a quantity and return that from a function.
@dimensions(x='Length', y='Mass')
def f(x, y):
x = x >> metre # Converts to metres, leaving a primitive float
y = y >> ounces # Converts to metres, leaving a primitive float
<code that assumes meters and ounces, returns value in BTU>
# Convert the primitive float to BTU on the way out
return answer * BTUThis way you can still easily wrap performance-critical calculations with robust unit-handling.
The inspiration for misu was
Frink by Alan Eliasen. It is
wonderful, but I need to work with units in the IPython Notebook, and
with all my other Python code.
There are a bunch of other similar projects. I have not used any of them enough yet to provide a fair comparison:
- astropy.units
- Buckingham
- DimPy
- Magnitude
- NumericalUnits
- Pint
- Python-quantities
- Scalar
- Scientific.Physics.PhysicalQuantities
- SciMath
- sympy.physics.units
- udunitspy
- Units
- Unum
Publishing to PyPI is automated via GitHub Actions and PyPI trusted
publishing (OIDC). No API
tokens or passwords are stored anywhere — PyPI trusts the
cjrh/misu repo's release.yml workflow running in the pypi
environment, and rejects everything else.
The version lives in Cargo.toml. pyproject.toml declares
dynamic = ["version"], so maturin reads it from the Rust crate at
build time — there is only one place to edit.
Use cargo-release to
bump, commit, tag, and push in one step. From a clean master:
cargo release patch --execute # 2.0.0 → 2.0.1
cargo release minor --execute # 2.0.0 → 2.1.0
cargo release major --execute # 2.0.0 → 3.0.0
cargo release 2.0.5 --execute # explicit versionDrop --execute for a dry run.
cargo-release performs the following steps locally:
- Bumps
versioninCargo.tomland updatesCargo.lock. - Commits the change with the message
Release <version>. - Creates an annotated tag
v<version>. - Pushes the branch and the tag to
origin.
The tag push triggers .github/workflows/release.yml, which:
- Builds wheels for Linux (x86_64, aarch64), macOS (x86_64, aarch64),
and Windows (x64), plus an sdist. Because PyO3 is configured with
abi3-py39, one wheel per (OS, arch) covers all supported Python versions. - Downloads all artifacts into the
releasejob, which runs in thepypiGitHub environment. - Uploads to PyPI via
pypa/gh-action-pypi-publish. Authentication happens via OIDC against the trusted-publisher configuration on PyPI; nothing else is needed.
If the build jobs succeed but the release job fails (for example, the PyPI environment was not configured), nothing is published, and the release can be retried by re-running just the failed job.
- cargo-release refuses with "uncommitted changes" — commit or stash first; cargo-release insists on a clean tree.
- cargo-release refuses with "not on allowed branch" — release only
from
master(the defaultallow-branchsetting). - PyPI rejects the upload with "version already exists" — PyPI filenames are immutable; bump again.
- Tag pushed but workflow did not run — check the tag matches the
tags: '*'trigger inrelease.ymland that thepypiGitHub environment exists with no protection rules blocking the run.