# Pint recipes Verified against pint 0.25.3 with NumPy 2.5.1. Every output below was produced by running the snippet. ## One registry per process A `Quantity` belongs to the registry that created it. Two registries produce quantities that cannot interact, and the failure is a bare `ValueError` far from the cause: ```python import pint first = pint.UnitRegistry() second = pint.UnitRegistry() first.Quantity(1, "m") + second.Quantity(1, "m") # ValueError: Cannot operate with Quantity and Quantity of different registries. ``` This bites hardest across module boundaries, where each module innocently creates its own registry at import time, and after unpickling, because a pickled quantity is restored against the *application* registry rather than the one that created it. Build one registry and share it, or use the application registry everywhere: ```python import pint ureg = pint.UnitRegistry() pint.set_application_registry(ureg) # in every other module ureg = pint.get_application_registry() ``` ## Offset units Degrees Celsius and Fahrenheit measure a point on a scale, not an amount, so multiplication and addition are undefined for them. Pint refuses rather than guessing: ```python Q = ureg.Quantity Q(20, "degC") * 2 # OffsetUnitCalculusError: Ambiguous operation with offset unit (degree_Celsius). Q(20, "degC") + Q(5, "degC") # OffsetUnitCalculusError: Ambiguous operation with offset unit (...). ``` The delta units carry temperature *differences*, and mixed arithmetic works: ```python Q(20, "degC") + Q(5, "delta_degC") # 25 degree_Celsius Q(25, "degC") - Q(20, "degC") # 5 delta_degree_Celsius ``` Note the second line: subtracting two absolute temperatures yields a delta unit automatically, which is correct and often surprising downstream. An uncertainty on a temperature is always a difference. `u = 0.5 degC` means `0.5 delta_degC`; converting it to Fahrenheit multiplies by 9/5 and applies no offset, giving `0.9 delta_degF`. Converting the *value* 20 degC to Fahrenheit applies the offset and gives 68 degF. Two different conversions on the same line of a report. `pint.UnitRegistry(autoconvert_offset_to_baseunit=True)` makes arithmetic proceed by converting to kelvin first. It removes the exception, not the ambiguity; enable it deliberately, not to silence an error. ## Logarithmic units Pint models dB, dBm, and friends as non-multiplicative units, and `+` on them means what it means in log space — multiplication of the underlying linear quantities: ```python Q(10, "dBm").to("mW") # 10.000000000000002 milliwatt Q(10, "dBm") + Q(10, "dBm") # 0.00010000000000000005 kilogram**2 * meter**4 / second**6 ``` The second line is 10 mW × 10 mW = 10^-4 W², not 20 mW and not 13 dBm. Nothing raises. Convert to a linear unit, do the arithmetic, convert back. ## Contexts Some conversions are physical relations rather than dimensional identities. Pint performs them only inside a named context, which is a feature: it forces the physics to be stated. ```python Q(532, "nm").to("THz", "sp") # 563.5196578947367 terahertz Q(532, "nm").to("1/cm", "sp") # 18796.992481203004 / centimeter Q(532, "nm").to("eV", "sp") # 2.3305300457368467 electron_volt Q(532, "nm").to("THz") # DimensionalityError Q(1, "g").to("mol", "chemistry", mw=Q(180.156, "g/mol")) # 0.005550744909966918 mole Q(298.15, "K").to("eV", "boltzmann") # 0.02569257912108585 electron_volt Q(1, "gauss").to("T", "Gaussian") # 9.999999999338245e-05 tesla ``` The registry ships `spectroscopy` (`sp`), `chemistry` (`chem`), `boltzmann`, `energy`, `textile`, `Gaussian` (`Gau`), and `ESU` (`esu`). `ureg.enable_contexts("sp")` turns one on for every subsequent conversion and `ureg.disable_contexts()` turns it off again; prefer passing the context per call so the assumption stays visible at the point of use. The spectroscopy context accepts a refractive index `n`, defaulting to 1 (vacuum). It matters more than it looks: ```python Q(532, "nm").to("THz", "sp") # 563.5196578947367 terahertz Q(532, "nm").to("THz", "sp", n=1.33) # 423.69899089829823 terahertz ``` Note that `gauss` fails without the Gaussian context: CGS electromagnetic units have different *dimensions* from SI ones, not merely different scales. ## Stripping the unit `.magnitude` returns whatever number the quantity happens to be carrying, in whatever unit it happens to be in. That is the single most common way a unit error enters a correct-looking program: ```python length = (12.7 * ureg.mm).magnitude # 12.7 -- but of what? length = (12.7 * ureg.mm).to("m").magnitude # 0.0127 metres, stated length = (12.7 * ureg.mm).m_as("m") # same, shorter ``` Always name the unit at the point of extraction. `m_as` exists precisely so there is no excuse. ## Boundary enforcement Rather than sprinkling conversions through a function, convert once at its boundary: ```python @ureg.wraps("J", ("N", "m")) def work(force, distance): return force * distance work(ureg.Quantity(2, "N"), ureg.Quantity(300, "cm")) # 6.0 joule ``` `wraps` strips the declared units on the way in and reattaches the result unit on the way out, so the body is plain floats and stays fast. It defaults to `strict=True`, which rejects bare numbers: ```python work(2.0, 3.0) # ValueError: A wrapped function using strict=True requires quantity or a string # for all arguments with not None units. ``` `strict=False` accepts bare numbers and assumes they are already in the declared units. That is convenient and it is also exactly the assumption that unit tracking exists to avoid; use it only at an edge you control. `check` validates dimensionality without converting: ```python @ureg.check("[length]", "[time]") def speed(distance, elapsed): return distance / elapsed speed(ureg.Quantity(10, "kg"), ureg.Quantity(2, "s")) # DimensionalityError: Cannot convert from '10 kilogram' ([mass]) to 'a quantity of' ([length]) ``` ## NumPy interoperability A quantity can wrap an array, and most ufuncs and many array functions are supported: ```python import numpy as np a = ureg.Quantity(np.array([1.0, 2.0, 3.0]), "m") np.mean(a) # 2.0 meter np.concatenate([a, ureg.Quantity(np.array([100.0]), "cm")]) # [1.0 2.0 3.0 1.0] meter np.concatenate([a, np.array([1.0])]) # DimensionalityError: Cannot convert from 'dimensionless' to 'meter' ``` Note that the mixed concatenation converted centimetres to metres correctly, and the bare array was rejected rather than assumed. Both behaviours are what you want. Wrapped arrays carry per-operation overhead. In an inner loop, convert at the boundary with `wraps` or `m_as` and compute on raw arrays. ## Custom units and definitions ```python ureg.define("cell = [cell_count] = cells") ureg.define("od600 = [optical_density]") ureg.define("percent_v_v = 0.01 = %v/v") ``` Defining a new base dimension in square brackets makes it dimensionally distinct from everything else, which is the point: `cells / mL` will then refuse to be added to `particles / mL`. Load a whole file of them with `ureg.load_definitions("units.txt")`. ## Formatting ```python q = ureg.Quantity(1.2345, "kg*m/s**2") f"{q}" # 1.2345 kilogram * meter / second ** 2 f"{q:~}" # 1.2345 kg * m / s ** 2 f"{q:.3f~P}" # 1.234 kg·m/s² f"{q:~L}" # 1.2345\ \frac{\mathrm{kg} \cdot \mathrm{m}}{\mathrm{s}^{2}} ``` `~` gives short unit symbols, `P` pretty Unicode, `L` LaTeX, `C` compact ASCII. Numeric format specs come first and behave as usual. ## With uncertainties The two libraries compose: a `ufloat` magnitude inside a pint quantity converts and formats correctly. ```python from uncertainties import ufloat q = ufloat(2.5, 0.1) * ureg.meter q.to("cm") # 250+/-10 centimeter f"{q:.2uS}" # 2.50(10) meter ``` ## Related packages `pint-pandas` provides a pandas extension dtype so a DataFrame column carries a unit; `pint-xarray` does the same for xarray. Both are separate installs and both inherit the one-registry rule.