A Python library for computing with units
Updated 2025-01-28:
magnitude
has been modernized with full type hints, improved error handling, and is available on PyPI.
magnitude
implements efficient computation with values with units. It allows you to do mathematical operations with them as if they were numbers, taking care of the units behind the scenes.
Units are specified as strings. They can be any of the SI units, plus a bunch of non-SI, bits, dollars, and any combination of them. They can include the standard SI prefixes. magnitude
can operate with physical quantities, parse their units, and print them. You don’t have to worry about unit consistency or conversions; everything is handled transparently.
By default output is done either in the unit with which the magnitude
object has been created, or in basic SI units, but you can specify any output unit, as long as it can be reduced to the basic units of the physical quantity.
All standard prefixes are understood, from yocto to yotta and from kibi to exbi.
https://github.com/juanre/magnitude
Installation
magnitude
is available on PyPI, install it with:
uv add magnitude
or using pip:
pip install magnitude
For development clone the repository:
git clone https://github.com/juanre/magnitude.git
cd magnitude
uv sync
What’s New in v1.1.1
- Unicode superscript support for beautiful unit notation:
- Input:
mg(10, 'm²/s²')
or traditionalmg(10, 'm^2/s^2')
- Output: Enable with
unicode_superscript(True)
or set environment variableMAGNITUDE_UNICODE_SUPERSCRIPTS=1
- Input:
- Full type hints throughout the codebase for better IDE support
- Improved error handling with specific exception types (
UnitError
,IncompatibleUnitsError
,ConversionError
) - Modern packaging - Now available on PyPI, installable via
pip
anduv
- Better REPL experience - No need to
print()
magnitudes, they display nicely by default - Python 3.9+ support with f-strings and modern Python features
- Development tools - Pre-commit hooks, pytest configuration, mypy type checking
Examples
>>> from magnitude import mg
>>>
>>> # Basic usage - traditional notation
>>> mg(10, 'm/s') ** 2
100.0000 m^2 / s^2
>>>
>>> # You can also use Unicode superscripts in input
>>> mg(10, 'm²/s²')
10.0000 m^2 / s^2
>>>
>>> # Enable Unicode output display
>>> from magnitude import unicode_superscript
>>> unicode_superscript(True)
>>> mg(10, 'm/s') ** 2
100.0000 m² / s²
>>>
>>> # More examples
>>> (mg(10, 'm') * 2 / mg(10, 'm/s²')).sqrt()
1.4142 s
>>> tsq = mg(10, 'm') * 2 / mg(10, 'm/s²')
>>> tsq ** 0.5
1.4142 s
>>>
>>> # Working with different units
>>> mg(1, "lightyear") / mg(1, "c")
31557600.0000 s
>>> y = mg(1, "lightyear") / mg(1, "c")
>>> y.ounit("year")
1.0000 year
>>> y.ounit('day')
365.2500 day
>>>
>>> # Power calculation
>>> power = mg(100, 'kg') * mg(2, 'gravity') * mg(5, 'km/h')
>>> power
2724.0694 m² kg / s³
>>> power.ounit('W')
2724.0694 W
>>> power.ounit('kW')
2.7241 kW
Unicode Superscript Support
magnitude
v1.1.1 introduces Unicode superscript support for more readable unit notation:
Enabling Unicode Output
There are three ways to enable Unicode superscript output:
Programmatically (per session):
from magnitude import unicode_superscript unicode_superscript(True)
Environment variable (persistent):
export MAGNITUDE_UNICODE_SUPERSCRIPTS=1 # or 'true', 'yes', 'on'
In your Python script:
import os os.environ['MAGNITUDE_UNICODE_SUPERSCRIPTS'] = '1'
Unicode Input
You can use Unicode superscripts directly in unit strings:
>>> mg(10, 'm²') # Unicode superscript 2
10.0000 m²
>>> mg(5, 's⁻¹') # Negative exponent
5.0000 s⁻¹
>>> mg(1, 'kg¹²') # Multi-digit exponent
1.0000 kg¹²
Traditional caret notation (^
) still works:
>>> mg(10, 'm^2') # Same as m²
10.0000 m²
>>> mg(5, 's^-1') # Same as s⁻¹
5.0000 s⁻¹
Reference
Available units
The basic units understood by the magnitude module are:
Indicator | Meaning |
---|---|
$ | dollar |
A | ampere |
b | bit |
cd | candela |
K | degrees Kelvin |
kg | kilograms |
m | meters |
mol | amount of substance |
s | seconds |
From these basic units you can derive all other units. The magnitude package predefines these derived units:
# `magnitude`s for the base SI units
new_mag('m', Magnitude(1.0, m=1))
new_mag('s', Magnitude(1.0, s=1))
new_mag('K', Magnitude(1.0, K=1))
new_mag('kg', Magnitude(1.0, kg=1))
new_mag('A', Magnitude(1.0, A=1))
new_mag('mol', Magnitude(1.0, mol=1))
new_mag('cd', Magnitude(1.0, cd=1))
new_mag('$', Magnitude(1.0, dollar=1))
new_mag('dollar', Magnitude(1.0, dollar=1))
new_mag('b', Magnitude(1.0, b=1)) # bit
# Magnitudes for derived SI units
new_mag('B', Magnitude(8.0, b=1))
new_mag('rad', Magnitude(1.0)) # radian
new_mag('sr', Magnitude(1.0)) # steradian
new_mag('Hz', Magnitude(1.0, s=-1)) # hertz
new_mag('g', Magnitude(1e-3, kg=1)) # gram
new_mag('N', Magnitude(1.0, m=1, kg=1, s=-2)) # newton
new_mag('Pa', Magnitude(1.0, m=-1, kg=1, s=-2)) # pascal
new_mag('J', Magnitude(1.0, m=2, kg=1, s=-2)) # joule
new_mag('W', Magnitude(1.0, m=2, kg=1, s=-3)) # watt
new_mag('C', Magnitude(1.0, s=1, A=1)) # coulomb
new_mag('V', Magnitude(1.0, m=2, kg=1, s=-3, A=-1)) # volt
new_mag('F', Magnitude(1.0, m=-2, kg=-1, s=4, A=2)) # farad, C/V
new_mag('ohm', Magnitude(1.0, m=2, kg=1, s=-3, A=-2)) # ohm, V/A
new_mag('S', Magnitude(1.0, m=-2, kg=-1, s=3, A=2)) # siemens, A/V, el cond
new_mag('Wb', Magnitude(1.0, m=2, kg=1, s=-2, A=-1)) # weber, V.s, mag flux
new_mag('T', Magnitude(1.0, kg=1, s=-2, A=-1)) # tesla, Wb/m2, mg flux dens
new_mag('H', Magnitude(1.0, m=2, kg=1, s=-2, A=-2)) # henry, Wb/A, induct.
new_mag('degC', Magnitude(1.0, K=1)) # celsius, !!
new_mag('lm', Magnitude(1.0, cd=1)) # lumen, cd.sr (=cd)), luminous flux
new_mag('lux', Magnitude(1.0, m=-2, cd=1)) # lux, lm/m2, illuminance
new_mag('Bq', Magnitude(1.0, s=-1)) # becquerel, activity of a radionulide
new_mag('Gy', Magnitude(1.0, m=2, s=-2)) # gray, J/kg, absorbed dose
new_mag('Sv', Magnitude(1.0, m=2, s=-2)) # sievert, J/kg, dose equivalent
new_mag('kat', Magnitude(1.0, s=-1, mol=1)) # katal, catalitic activity
# Non-SI but almost:
new_mag('b', Magnitude(8.0, b=1)) # byte, note that B is Bel, as in dB
### Other
# length
new_mag("'", Magnitude(0.3048, m=1)) # feet
new_mag('ft', Magnitude(0.3048, m=1)) # feet
new_mag('inch', Magnitude(0.0254, m=1)) # inch
new_mag('"', Magnitude(0.0254, m=1)) # inch
new_mag('lightyear', Magnitude(2.99792458e8 * 365.25 * 86400, m=1))
# volume
new_mag('l', Magnitude(0.001, m=3))
# time
# year is tropical year, "the mean interval between vernal
# equinoxes. Differs from the sidereal year by 1 part in 26000
# due to precession of the earth about its rotational axis
# combined with precession of the perihelion of the earth's orbit"
# (from units.dat).
new_mag('year', Magnitude(31556925.974678401, s=1))
new_mag('day', Magnitude(86400, s=1))
new_mag('h', Magnitude(3600, s=1))
new_mag('min', Magnitude(60, s=1))
# Resolution
new_mag('dpi', Magnitude(1.0 / 0.0254, m=-1))
new_mag('lpi', Magnitude(1.0 / 0.0254, m=-1))
# Velocity
new_mag('ips', Magnitude(0.0254, m=1, s=-1))
new_mag('c', Magnitude(2.99792458e8, m=1, s=-1))
# Acceleration
new_mag('gravity', Magnitude(9.80665, m=1, s=-2))
Two magnitudes have no units, ‘rad’ (radian - unit of plane angle) and ‘sr’ (steradian - unit of solid angle).
Any of the above units can be augmented with the following set of scale prefixes:
_prefix = {'y': 1e-24, # yocto
'z': 1e-21, # zepto
'a': 1e-18, # atto
'f': 1e-15, # femto
'p': 1e-12, # pico
'n': 1e-9, # nano
'u': 1e-6, # micro
'm': 1e-3, # mili
'c': 1e-2, # centi
'd': 1e-1, # deci
'k': 1e3, # kilo
'M': 1e6, # mega
'G': 1e9, # giga
'T': 1e12, # tera
'P': 1e15, # peta
'E': 1e18, # exa
'Z': 1e21, # zetta
'Y': 1e24, # yotta
# Binary prefixes, approved by the International
# Electrotechnical Comission in 1998. Since then, kb means
# 1000 bytes; for 1024 bytes use Kib (note the capital K in
# the binary version, and the lower case for the b of byte,
# see comment in byte definition below).
'Ki': 2 ** 10, # Kibi (<- kilo, 10^3)
'Mi': 2 ** 20, # Mebi (<- mega, 10^6)
'Gi': 2 ** 30, # Gibi (<- giga, 10^9)
'Ti': 2 ** 40, # Tebi (<- tera, 10^12)
'Pi': 2 ** 50, # Pebi (<- peta, 10^15)
'Ei': 2 ** 60 # Exbi (<- exa, 10^18)
}
Exported symbols
Magnitude
[class] — Numbers with units; math operations are overloadedmg(number, unit, ounit='')
— Construct aMagnitude
ensmg(m, unit='')
— Tries to build aMagnitude
out of somethingnew_mag(indicator, mag)
— Intern a new magnitude with its nameunicode_superscript(enable: bool)
— Enable/disable Unicode superscript outputMagnitudeError
[class] — Base exception formagnitude
errorsUnitError
[class] — Raised when an unknown unit string is encounteredIncompatibleUnitsError
[class] — Raised for operations on incompatible unitsConversionError
[class] — Raised when a value cannot be converted to a Magnitude__version__
— Version string for themagnitude
package
Defining new magnitudes
You can define new magnitudes by instantiating the Magnitude
class. Suppose you want to define pounds as a magnitude and associate with it the unit ’lb’. A pound is 0.45359237 kilograms, so we have
>>> from magnitude import Magnitude, new_mag
>>> lb = Magnitude(0.45359237, kg=1)
To make it recognized automatically you also have to introduce it to the system with new_mag
. You can then use it as you would any other predefined physical quantity:
>>> new_mag('lb', lb)
>>> me = mg(180, 'lb')
>>> me.ounit('kg').toval()
81.6466266
>>> new_mag('mile', mg(160934.4, 'cm'))
>>> mg(100, 'mile/h').ounit('km/h')
160.9344 km/h
Error Handling
magnitude
v1.1.1 introduces a comprehensive exception hierarchy for better error handling:
>>> from magnitude import mg, UnitError, IncompatibleUnitsError, ConversionError
>>>
>>> # Unknown unit raises UnitError
>>> try:
... mg(10, 'invalid_unit')
... except UnitError as e:
... print(f"Unknown unit: {e}")
Unknown unit: Unit string invalid_unit has unit <invalid_unit> that has not been defined
>>>
>>> # Incompatible units raise IncompatibleUnitsError
>>> try:
... mg(10, 'm').ounit('s') # Can't convert meters to seconds
... except IncompatibleUnitsError as e:
... print(f"Incompatible: {e}")
Incompatible: Cannot convert m¹ to s¹: incompatible units
>>>
>>> # The exception provides details about the operation
>>> try:
... mg(10, 'm') + mg(5, 's')
... except IncompatibleUnitsError as e:
... print(f"Operation: {e.operation}")
... print(f"Left unit: {e.left_unit}")
... print(f"Right unit: {e.right_unit}")
Operation: add
Left unit: m¹
Right unit: s¹
Bits and bytes (2009-11-03)
A previous version of the library used "bit" for bit and "b" for byte, leaving B for Bel. Following Michael Scheper’s suggestion we follow now IEEE 1541 and use "b" for bit and "B" for byte. If the need arises I’ll implement ad-hoc esupport for dB, but for the time being there is none.
More on units
This code was very much inspired by https://www.cs.utexas.edu/users/novak/units.html and its associated paper, https://www.cs.utexas.edu/users/novak/units95.html
The following online references provide more detail about physical units and the SI system.
- https://physics.nist.gov/cuu/Units/units.html
- https://en.wikipedia.org/wiki/SI
- https://www.gnu.org/software/units/units.html for units.dat
- https://www.cip.physik.uni-muenchen.de/~tf/misc/etools.lisp
Alternative unit libraries for Python
I haven’t tested them; I wrote these comments several years ago after looking at the documentation.
- Unum looks well finished and is thoroughly documented. Good: allows you to define arbitrary units (magnitude only supports user-defined units as long as they are a combination of the base units). Bad: it looks like it doesn’t handle prefixes, and it clutters your namespace with all its unit definitions (you end up with variables named M, S, etc. in your namespace).
- Scalar looks very similar to Unum, maybe not as well documented. Looks like it has exactly the same problems and advantages, plus the ability of running with units disabled (which should be good for performance).
- PhyisicalQuantities, part of ScientificPython, is very similar to magnitude. Same approach, essentially the same functionality, very similar API. Looks like it also clutters your namespace with unit names.