Python library to represent numbers with units
Project description
units
The Price of Unitless Arithmetic
On September 23, 1999, flight controllers expected NASA's Mars Climate Orbiter to pass behind Mars, fire its engine, and come back into radio contact after orbit insertion. It never came back. When engineers reviewed the final hours of flight data, the trajectory was not where the navigation system thought it was: the spacecraft had approached Mars far lower than planned. The investigation traced the loss to a unit boundary that software had failed to defend. One side of the system handled "small forces" data in English units; the navigation side expected metric units.
That is the kind of bug this package is meant to stop. Without units, the mistake is just arithmetic:
# A navigation routine expects impulse in newton-seconds.
expected_impulse_ns = 120
# A supplier routine accidentally sends a value in a different force unit.
# The number is still just a number, so Python accepts it.
reported_impulse_other_units = 120
trajectory_impulse = expected_impulse_ns + reported_impulse_other_units
print(trajectory_impulse) # 240
There is nothing in 240 that tells you a spacecraft trajectory may now be
wrong. With units attached, the mismatch stops at the boundary:
from units import CustomUnitBase
from units.dimension import DimensionSystem
from units.si import newton, second
class EnglishImpulseUnit(CustomUnitBase):
dimension_system = DimensionSystem("english-impulse", ("lbf_s",))
pound_force_second = EnglishImpulseUnit.define("lbf_s")
expected_impulse = 120 * newton * second
reported_impulse = 120 * pound_force_second
trajectory_impulse = expected_impulse + reported_impulse
# UnitCompatibilityError: units mismatch: m·kg·s^-1 and lbf_s
That failure is the feature. A bug that would otherwise move through a program as an ordinary number is stopped before it contaminates mission-critical calculations.
Background: NASA/JPL describe the Mars Climate Orbiter loss as a navigation error caused by a failure to translate English units to metric, sending the spacecraft too close to Mars.
Source: https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
About
python-units is a Python package for unit-aware arithmetic. It provides:
- a
Quantitytype that combines numeric values with unit information - a registry of SI base and derived units
- algebraic unit manipulation and compatibility checks
- explicit multiplicative and affine conversions between compatible units
- common imperial and US customary units in
units.imperial - a public API that prioritizes scalar-by-unit construction and SI unit imports
- a migration path from the legacy
Unitconstructor and compatibility helpers - a Python 3-only codebase with no Python 2 compatibility shims
- a project structure that separates public API, core logic, data models, and utilities
- comprehensive unit tests and documentation
Supported Python versions: 3.10+
Python 2 is not supported.
Project layout:
- public facade:
src/units - API exports:
src/api - business logic:
src/core - data models:
src/models - utilities:
src/utils - tests:
tests/unitandtests/integration
Preferred API:
from units import Quantity
from units.si import metre, second, newton
distance = 10 * metre
time = 2 * second
speed = distance / time
force = 5 * newton
print(distance)
print(speed)
print(force)
The preferred construction style is scalar-by-unit multiplication:
from units.si import metre, second
length = 3 * metre
time = 2 * second
speed = length / time
volume = 5 * metre ** 3
Because ** binds more tightly than *, 5 * metre ** 3 is interpreted as
5 * (metre ** 3), which is the intended geometric-unit behavior.
The explicit constructor remains supported and is still the right low-level form when you want to be fully explicit:
from units import Quantity
from units.si import metre
length = Quantity(3, metre)
Legacy API compatibility:
import units as u
print(u.Unit(1, u.metre))
The legacy Unit constructor remains available as a compatibility alias for
Quantity during the migration period. It is deprecated and scheduled for
removal in 1.0.0, but it remains a true alias until then so existing type
checks keep working. New code should prefer from units import Quantity and
from units.si import ....
The package is Python 3-only. Python 2 compatibility behavior is not part of the supported interface.
Migration guide
Old style:
import units as u
distance = u.Unit(3, u.metre)
time = u.Unit(2, u.second)
speed = distance / time
New style:
from units.si import metre, second
distance = 3 * metre
time = 2 * second
speed = distance / time
volume = 5 * metre ** 3
Still supported when you want the fully explicit constructor form:
from units import Quantity
from units.si import metre, second
distance = Quantity(3, metre)
time = Quantity(2, second)
speed = distance / time
Public API
Stable top-level imports:
QuantityUnit(compatibility alias forQuantity)convertvalueunitmultiplierUnitsError,InvalidUnitError,InvalidValueError,UnitCompatibilityError,UnitOperandError
Canonical unit imports:
from units.si import metre, second, newton- prefixed and scaled units such as
kilometre,centimetre,gram,minute,hour,kilowatt, andmillivolt from units.imperial import inch, foot, yard, mile, ounce, pound, fahrenheit
Legacy compatibility helpers:
Unitlong_quantityint_unitfloat_unitlong_unitcomplex_unit
These names remain available during the migration period and emit
DeprecationWarning when called. Unit remains a true alias for Quantity and
does not emit a call-time warning, because preserving Unit is Quantity is part
of the pre-1.0.0 compatibility contract. New code should prefer Quantity,
scalar-by-unit construction, and the *_quantity conversion helpers. The
deprecated compatibility paths are scheduled for removal in 1.0.0.
Notes on semantics
- Addition and subtraction require identical units.
- Multiplication and division combine units algebraically.
- Explicit conversions are available through
quantity.to(unit)andconvert(quantity, unit). - Scale-only conversions cover prefixed SI and imperial units.
- Affine conversions cover Celsius, Fahrenheit, and kelvin.
- Integer powers of units and unit-bearing quantities are supported.
- Unitless quantities are supported explicitly.
- Affine units such as Celsius and Fahrenheit are blocked from multiplicative arithmetic because offsets make those operations ambiguous.
- The core quantity model allows signed values. Domain-specific constraints such as non-negative lengths should be enforced by higher-level types or validators.
Conversion foundations
0.4.0 adds explicit multiplicative conversions. Conversion never happens
silently during addition or subtraction; you choose the target unit.
from units import convert, multiplier, unit, value
from units.si import gram, hour, kilogram, kilometre, metre, minute, second
distance = 1.5 * kilometre
print(distance.to(metre)) # 1500 m
print(convert(2500 * metre, kilometre)) # 2.5 km
duration = 2 * hour
print(duration.to(minute)) # 120 min
print((1500 * gram).to(kilogram)) # 1.5 kg
speed = (72 * kilometre) / (2 * hour)
print(speed) # 10.0 m·s^-1
print(value(distance)) # 1.5
print(unit(distance)) # km
print(multiplier(kilometre)) # 1000.0
0.5.0 extends conversion to affine temperature units and common imperial
units. Conversion still never happens silently during addition or subtraction;
you choose the target unit.
0.5.0 examples
The 0.5.0 release adds three practical conversion capabilities: affine
temperature conversion, imperial units, and familiar composite display units.
from units import convert
from units.imperial import fahrenheit, foot, mile, pound
from units.si import (
degree_celcius,
gram,
hour,
kelvin,
kilometre,
metre,
picometre,
)
# Affine conversions include the offset.
print(convert(0 * degree_celcius, kelvin)) # 273.15 K
print(convert(32 * fahrenheit, degree_celcius)) # 0 °C
# Imperial units convert explicitly into SI or familiar metric units.
print(convert(3 * foot, metre)) # 0.9144 m
print(convert(1 * pound, gram)) # 453.59237 g
# Composite target units preserve the display unit you ask for.
speed = 60 * mile / hour
print(convert(speed, kilometre / hour)) # 96.56064 km·h^-1
# Pico-prefixed units are part of the prefixed SI set.
print(convert(1000000000000 * picometre, metre)) # 1 m
The same strict arithmetic rules still apply. Compatible conversion is explicit; addition and subtraction still require identical units.
Affine temperature conversions
Temperature scales such as Celsius and Fahrenheit require an offset. These are supported through explicit conversion:
from units import convert
from units.imperial import fahrenheit
from units.si import degree_celcius, kelvin
print(convert(0 * degree_celcius, kelvin)) # 273.15 K
print(convert(32 * fahrenheit, degree_celcius)) # 0 °C
print((100 * degree_celcius).to(fahrenheit)) # 212 °F
Affine units are not allowed in multiplicative arithmetic:
from units.si import degree_celcius, second
temperature = 20 * degree_celcius
temperature * second
# UnitCompatibilityError: units cannot be combined multiplicatively: °C and s
This is deliberate. Absolute temperatures and temperature intervals are different semantic concepts; constrained semantic types are planned for a later release.
Imperial and US customary units
Common non-SI units live in units.imperial:
from units import convert
from units.imperial import foot, mile, pound
from units.si import gram, hour, kilometre, metre
print(convert(3 * foot, metre)) # 0.9144 m
print(convert(1 * mile, kilometre)) # 1.609344 km
print(convert(1 * pound, gram)) # 453.59237 g
speed = 60 * mile / hour
print(convert(speed, kilometre / hour)) # 96.56064 km·h^-1
Prefixed and scaled units
Common SI prefixes and practical time units are available from units.si:
from units.si import (
centimetre,
gram,
hour,
kiloampere,
kilometre,
kilovolt,
kilowatt,
megawatt,
micrometre,
microsecond,
milliampere,
milligram,
millimetre,
millisecond,
millivolt,
milliwatt,
minute,
nanometre,
nanosecond,
picogram,
picometre,
picosecond,
tonne,
)
Scaled units participate correctly in multiplication, division, and powers:
from units.si import hour, kilometre, metre
area = (2 * kilometre) * (3 * metre)
print(area) # 6000 m^2
square = (2 * kilometre) ** 2
print(square) # 4000000 m^2
speed = (72 * kilometre) / (2 * hour)
print(speed) # 10.0 m·s^-1
Familiar composite units
Composite unit expressions such as kilometre / hour are algebraic unit
definitions. Direct arithmetic renders in canonical SI base form:
from units.si import hour, kilometre
speed = 30 * kilometre / hour
print(speed) # 8.333333333333334 m·s^-1
When you want a semantically familiar display unit, convert to that composite unit or give it an explicit name:
from units import DerivedUnit, convert
from units.si import hour, kilometre
kilometres_per_hour = DerivedUnit.define("km·hr^-1", kilometre / hour)
speed = 30 * kilometre / hour
print(convert(speed, kilometre / hour)) # 30 km·h^-1
print(convert(speed, kilometres_per_hour)) # 30 km·hr^-1
print(30 * kilometres_per_hour) # 30 km·hr^-1
This keeps the arithmetic deterministic while letting application code choose
domain-specific display names such as km·hr^-1, N·m, or any other familiar
derived unit form.
Real-world examples
Electrical engineering: from resistance to power dissipation
from units.si import ampere, ohm, volt, watt
current = 12 * ampere
resistance = 8 * ohm
voltage = current * resistance
power = voltage * current
print(voltage) # 96 V
print(power) # 1152 W
This works because the package canonicalizes unambiguous derived-unit assemblies:
ampere * ohm -> voltvolt * ampere -> watt
Pump sizing: hydraulic power from pressure rise and flow rate
from units.si import metre, second, kilogram, pascal, watt
density = 998 * (kilogram / metre ** 3)
flow_velocity = 2.5 * (metre / second)
pipe_area = 0.0314 * metre ** 2
pressure_rise = 180000 * pascal
volumetric_flow = flow_velocity * pipe_area
hydraulic_power = pressure_rise * volumetric_flow
print(volumetric_flow) # m^3·s^-1
print(hydraulic_power) # W
This is a good example of a multi-step engineering computation that still renders to intuitive derived units at the end of the chain.
Structural mechanics: work from force over distance
from units.si import metre, newton
force = 4200 * newton
displacement = 0.35 * metre
work = force * displacement
print(work) # J
Geometric quantities: powers of units
from units.si import metre
volume = 5 * metre ** 3
area = (12 * metre) ** 2
print(volume) # 5 m^3
print(area) # 144 m^2
The unit form is also valid on its own:
from units.si import metre
area_unit = metre ** 2
volume_unit = metre ** 3
Fluid mechanics: dynamic pressure
from units.si import kilogram, metre, pascal, second
density = 1.225 * (kilogram / metre ** 3)
velocity = 68 * (metre / second)
dynamic_pressure = 0.5 * density * velocity * velocity
print(dynamic_pressure) # Pa
Custom unit systems
Custom unit systems are supported, but they are intentionally separate from SI canonicalization. Use them when you want the same algebra and formatting behaviour without forcing your units into the SI registry.
from units import CustomUnitBase, DimensionSystem
class CommUnit(CustomUnitBase):
dimension_system = DimensionSystem('comm', ('b', 's', 'B'))
bit = CommUnit.define('b')
second = CommUnit.define('s')
data = 32 * bit
duration = 4 * second
rate = data / duration
print(rate) # 8.0 b·s^-1
Custom systems inherit useful behaviour:
- dimensional algebra
- string rendering
- incompatibility checks within a system
They do not automatically simplify into SI-derived names such as V, J, or
Pa, and they cannot be mixed with SI units unless you build an explicit bridge.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_units-0.5.0.tar.gz.
File metadata
- Download URL: python_units-0.5.0.tar.gz
- Upload date:
- Size: 24.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88c58a53ec096cfcb41ec0fe902f48babe4d90f78fa62022634e195f273d03ea
|
|
| MD5 |
4d9235931b45cb826cb1f98f107c9b5b
|
|
| BLAKE2b-256 |
569ae154233043f539534f3fc381eddd40fd9f58945ee5d55c0b36fff7036fc2
|
Provenance
The following attestation bundles were made for python_units-0.5.0.tar.gz:
Publisher:
publish.yml on sci2pro/python-units
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_units-0.5.0.tar.gz -
Subject digest:
88c58a53ec096cfcb41ec0fe902f48babe4d90f78fa62022634e195f273d03ea - Sigstore transparency entry: 1531039771
- Sigstore integration time:
-
Permalink:
sci2pro/python-units@1216789401044b218feb7bdac6c5a1970e290e18 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/sci2pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1216789401044b218feb7bdac6c5a1970e290e18 -
Trigger Event:
release
-
Statement type:
File details
Details for the file python_units-0.5.0-py3-none-any.whl.
File metadata
- Download URL: python_units-0.5.0-py3-none-any.whl
- Upload date:
- Size: 24.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01e1f57e4e4e5f0fa8a23f7e5debd1951b291b4488cd9c822e4844b3367485fb
|
|
| MD5 |
ecd93aea3dc325e0748870af8762492b
|
|
| BLAKE2b-256 |
060494cfd0ddb2515cd4a7bf57b4f6d2b9b0b73e3ccd92fe9b93aec8bad9686b
|
Provenance
The following attestation bundles were made for python_units-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on sci2pro/python-units
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_units-0.5.0-py3-none-any.whl -
Subject digest:
01e1f57e4e4e5f0fa8a23f7e5debd1951b291b4488cd9c822e4844b3367485fb - Sigstore transparency entry: 1531039836
- Sigstore integration time:
-
Permalink:
sci2pro/python-units@1216789401044b218feb7bdac6c5a1970e290e18 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/sci2pro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1216789401044b218feb7bdac6c5a1970e290e18 -
Trigger Event:
release
-
Statement type: