kanne
Quick: is 0.0000001 one nanosecond? (...how many nanoseconds are in a second
again?) A bare number is a riddle. kanne makes you write "100 ns" and keeps it
that way — across Pydantic, JSON, GraphQL, the whole trip.
from kanne import Duration, Length
from kanne.kanne import Kanne
from pydantic import BaseModel
class Acquisition(BaseModel):
exposure: Duration # any time, your choice of unit
pixel_size: Length # any length
with Kanne():
a = Acquisition(exposure="100 ms", pixel_size="2.5 um")
a.model_dump() # {'exposure': '100 ms', 'pixel_size': '2.5 µm'}
a.exposure.to("s") # <Quantity(0.1, 'second')> — convert when *you* ask
a.exposure + a.pixel_size # 💥 can't add a time to a length
Acquisition(exposure=100, pixel_size="1 m") # 💥 100 what?
Acquisition(exposure="3 m", pixel_size="1 m") # 💥 a metre isn't a duration
Fields are typed by dimension (Duration, Length, ElectricPotential), not by a
fixed unit. They take any unit of the right dimension, keep the one you gave, and reject
bare numbers and nonsense at the door.
The point
One value, one self-describing token. No second _unit field to drift out of sync, no
"everyone just knows it's milliseconds" convention living in a docstring:
{ "exposure": 1500, "exposure_unit": "ms" } // 😬 two facts, can desync
{ "exposure": "1500 ms" } // 🎉 one fact, can't
And provenance: if the user typed 100 ms, it stays 100 ms — not 0.1 s, not
2.4999999999999996 from a round-trip through some base unit. The number you sent is
the number that arrives, bit for bit. Convert only when you mean to.
kanne is a wire protocol, not a database protocol. It moves quantities across boundaries intact. Storing them is the backend's job:
kanne_serversplits each value into{magnitude, unit}columns so SQL canSUM()/AVG()/range-query the number while keeping the original unit, and the GraphQL API serves it back either way —durationas the Pint string,durationAs("ms")as a plain number for clients that don't speak Pint. So: send the string, store the pair.
"Why not just store milliseconds as a float?"
Honestly? Often you should. A float is tiny, numeric, dependency-free, and a fixed unit
is a perfectly good contract — NEURON runs the world's biophysical models on exactly
that (dt, v, tstop = ms, mV) at huge scale. Inside a component that agrees on
the unit, the float wins, and that's literally how kanne_server stores things.
The float only cracks at the boundary, where the "it's in ms" convention has to travel out-of-band and isn't in the data:
- Wrong-unit bugs are silent. A reader assumes seconds, lands 1000× off, nothing complains. kanne raises instead.
- Your canonical unit ages badly. ms → µs spike timing, µm → nm optics, nM → pM
assays — every resolution bump means awkward sub-unit floats or a lockstep migration.
"25 ns"just... still works.
Float between your own functions; self-describing quantity between systems. kanne is only about the second half.
Install
uv add kanne
Built-in dimensions
Each is a real importable class (so codegen can name it), accepting any unit of its dimension:
| Type | e.g. | Type | e.g. |
|---|---|---|---|
Duration |
"5 ms" |
ElectricCurrent |
"5 pA" |
Frequency |
"1 kHz" |
ElectricPotential |
"-70 mV" |
Length |
"2.5 µm" |
ElectricCharge |
"2 nC" |
Area |
"4 µm**2" |
Capacitance |
"100 nF" |
Volume |
"5 µL" |
ElectricalConductance |
"2 µS" |
Velocity |
"3 µm/s" |
ElectricalResistance |
"100 MΩ" |
Mass |
"5 mg" |
Power |
"5 mW" |
Temperature |
"37 degC" |
Energy |
"5 mJ" |
AmountOfSubstance |
"5 mmol" |
Pressure |
"2 bar" |
Concentration |
"1 mM" |
Need another? One line:
from kanne import PintQuantity
class Acceleration(PintQuantity):
reference_unit = "meter / second ** 2" # declares the *dimension*, never a conversion target
Good to know
- The wire form is always a string — JSON schema is
string, JSON input must be a string, bare numbers are rejected. In Python apint.Quantityor another dimension instance is also accepted. - Still dimensionful at runtime —
Duration("5 ms") + Duration("1 s")→1005 ms;Duration("1000 ms") == Duration("1 s")→True. Wrong dimensions raise. - Context-aware registry —
Kanne()sets the active Pint registry on acontextvarfor thewithblock (defaults to a shared global;degC& friends work out of the box). Mixing registries raises rather than silently misbehaving.
Metadata
Release files for kanne 2.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| kanne-2.6.0.tar.gz | 18.6 kB | Without provenance | Details for kanne-2.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| kanne-2.6.0-py3-none-any.whl | Python 3 | none | any | Details for kanne-2.6.0-py3-none-any.whl |
Total release size: 38.4 kB
Release files / kanne-2.6.0.tar.gz
| Download URL | kanne-2.6.0.tar.gz |
|---|---|
| Size | 18.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
948ac31c01dc33a9cc72ad5e01944071ceae0858220a89706adf055a7dd63d70
|
|
BLAKE2b-256 checksum How to use checksums |
4f724f948fed59bc1eb0670002e7659bed64de151d86aeee76965382a1cb13bd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / kanne-2.6.0-py3-none-any.whl
| Download URL | kanne-2.6.0-py3-none-any.whl |
|---|---|
| Size | 19.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e2610c9c5c022d49579d877a61623bbeaa64cdc2889a2e9ced81748178f2e2a2
|
|
BLAKE2b-256 checksum How to use checksums |
8f1c1a55716d3181155b0ff187ad76a39f4c938e7031e54ad7f9b7159099ef65
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for kanne 2.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.