Intermediate
In 1999, NASA’s Mars Climate Orbiter was lost because one engineering team used metric units and another used imperial units — a 327 million dollar spacecraft burned up in the Martian atmosphere due to a unit mismatch. You are probably not building Mars orbiters, but if your code processes temperatures, distances, speeds, or any other physical quantity, you face the same class of bug every time a raw number crosses a unit boundary without carrying its unit along with it.
The Pint library solves this by treating numbers as quantities — values paired with units that travel together through every calculation. When you multiply meters by meters you get square meters. When you try to add kilometers to kilograms, Pint raises a DimensionalityError instead of silently returning nonsense. Install it with pip install pint.
In this article we will cover creating quantities, converting between units, doing arithmetic with dimensional analysis, defining custom units, formatting output, and integrating Pint with NumPy. We will finish with a unit-safe physics calculator that demonstrates why you never want to go back to bare floats for physical quantities.
Python Pint: Quick Example
Here is the core Pint workflow in under 20 lines. Create a unit registry, attach units to values, and let Pint handle conversions and validation automatically:
# quick_pint.py
from pint import UnitRegistry
ureg = UnitRegistry()
# Create quantities -- value + unit
distance = 10 * ureg.kilometer
time_taken = 30 * ureg.minute
speed = distance / time_taken
print(f'Distance: {distance}')
print(f'Time: {time_taken}')
print(f'Speed: {speed}')
print(f'Speed in km/h: {speed.to(ureg.kilometer / ureg.hour):.2f}')
print(f'Speed in mph: {speed.to("mph"):.2f}')
Output:
Distance: 10 kilometer
Time: 30 minute
Speed: 0.3333333333333333 kilometer / minute
Speed in km/h: 20.00 kilometer / hour
Speed in mph: 12.43 mile / hour
Notice that the speed calculation is done directly by dividing a distance by a time — Pint performs the arithmetic and tracks the compound unit automatically. The .to() method converts to any compatible unit with dimensional analysis. Want to go deeper? Below we cover the full Pint API including error handling, custom units, and NumPy integration.
What Is Pint and Why Use It?
Pint is a Python library for defining, operating, and manipulating physical quantities — numbers that have both a magnitude and a unit. The central object is the Quantity, which wraps a value with its unit and enforces dimensional consistency in all operations.
| Approach | Code Style | Unit Safety | Conversion Effort |
|---|---|---|---|
| Bare floats | speed = 20.0 | None — silent bugs | Manual everywhere |
| Comments | speed = 20.0 # km/h | None — comment ignored | Manual everywhere |
| Pint Quantity | speed = 20 * ureg.kph | Automatic DimensionalityError | Built-in with .to() |
The unit registry (UnitRegistry) knows about thousands of units built in — SI units, imperial units, derived units, and common abbreviations. You create it once per module and reuse it across your whole application.
Creating Quantities and the Unit Registry
There are several ways to create Pint quantities. The most common is multiplying a number by a unit attribute on the registry. You can also use string parsing for more flexible input:
# creating_quantities.py
from pint import UnitRegistry
ureg = UnitRegistry()
# Method 1: Multiply by registry attribute
mass = 5.5 * ureg.kilogram
temp = 100 * ureg.degC
area = 25 * ureg.meter ** 2
# Method 2: Parse from string (useful for user input)
length = ureg.Quantity('12.5 meters')
velocity = ureg.Quantity(60, 'km/hour')
# Method 3: Q_ shortcut (common in practice)
Q_ = ureg.Quantity
pressure = Q_(101325, 'Pa')
print(f'Mass: {mass}')
print(f'Temperature: {temp}')
print(f'Area: {area}')
print(f'Length (parsed): {length}')
print(f'Velocity (parsed): {velocity}')
print(f'Pressure: {pressure}')
# Access magnitude and units separately
print(f'\nMagnitude: {mass.magnitude}')
print(f'Units: {mass.units}')
print(f'Dimensionality: {mass.dimensionality}')
Output:
Mass: 5.5 kilogram
Temperature: 100 degree_Celsius
Area: 25 meter ** 2
Length (parsed): 12.5 meter
Velocity (parsed): 60 kilometer / hour
Pressure: 101325 pascal
Magnitude: 5.5
Units: kilogram
Dimensionality: [mass]
The string parsing form (ureg.Quantity('12.5 meters')) is particularly valuable when accepting unit input from users or configuration files — you do not have to parse the string yourself or validate the unit name. Pint handles both tasks and raises a UndefinedUnitError if the unit string is not recognized.
Converting Between Units
Unit conversion is where Pint saves the most code. The .to() method converts to any dimensionally compatible unit, and .to_base_units() normalizes to SI base units:
# conversions.py
from pint import UnitRegistry
ureg = UnitRegistry()
# Basic conversions
distance_km = 42.195 * ureg.kilometer # Marathon distance
print(f'Marathon: {distance_km.to(ureg.mile):.3f}')
print(f'Marathon: {distance_km.to(ureg.meter):.0f}')
print(f'Marathon: {distance_km.to("ft"):.0f}')
# Temperature conversions (offset units -- use .to() not arithmetic)
temp_c = 100 * ureg.degC
print(f'\n100 C in Fahrenheit: {temp_c.to(ureg.degF):.1f}')
print(f'100 C in Kelvin: {temp_c.to(ureg.kelvin):.2f}')
# Compound unit conversion
speed = 100 * ureg.kilometer / ureg.hour
print(f'\n100 km/h in m/s: {speed.to("m/s"):.2f}')
print(f'100 km/h in mph: {speed.to("mph"):.2f}')
# Force conversion
force = 100 * ureg.newton
print(f'\n100 N in pound-force: {force.to("lbf"):.3f}')
print(f'100 N in kgf: {force.to("kgf"):.3f}')
# to_base_units -- normalize to SI
energy = 1 * ureg.kilowatt_hour
print(f'\n1 kWh in base units: {energy.to_base_units():.0f}')
Output:
Marathon: 26.219 mile
Marathon: 42195 meter
Marathon: 138,434 foot
100 C in Fahrenheit: 212.0 degree_Fahrenheit
100 C in Kelvin: 373.15 kelvin
100 km/h in m/s: 27.78 meter / second
100 km/h in mph: 62.14 mile / hour
100 N in pound-force: 22.481 force_pound
100 N in kgf: 10.197 kilogram_force
1 kWh in base units: 3600000 kilogram * meter ** 2 / second ** 2
Temperature conversion deserves a special mention. Unlike most units, Celsius and Fahrenheit are offset units — 0 degrees Celsius is not the same as 0 degrees Kelvin. Pint handles this correctly: 100 * ureg.degC converts to 212 degF as expected. If you are doing temperature arithmetic (differences, not absolute temperatures), use ureg.delta_degC to get correct results for temperature intervals.
Dimensional Analysis and Error Prevention
The real value of Pint is not convenience — it is catching bugs that bare floats silently hide. When you add incompatible units, Pint raises a DimensionalityError immediately instead of returning a meaningless number:
# dimensional_analysis.py
from pint import UnitRegistry, DimensionalityError
ureg = UnitRegistry()
# Arithmetic with matching dimensions -- works correctly
d1 = 5 * ureg.kilometer
d2 = 3000 * ureg.meter
total = d1 + d2 # Pint converts automatically
print(f'Total distance: {total}')
print(f'In km: {total.to("km")}')
# Arithmetic with different-but-compatible dimensions
weight = 70 * ureg.kilogram
g = 9.81 * ureg.meter / ureg.second ** 2
force = weight * g
print(f'\nForce: {force:.2f}')
print(f'Force in N: {force.to(ureg.newton):.2f}')
# Dimensional incompatibility -- caught immediately
mass = 10 * ureg.kilogram
length = 5 * ureg.meter
try:
bad_result = mass + length # This makes no physical sense
except DimensionalityError as e:
print(f'\nCaught: {e}')
# Another bad operation
try:
speed = 60 * ureg.km / ureg.hour
nonsense = speed + mass
except DimensionalityError as e:
print(f'Caught: {e}')
Output:
Total distance: 8.0 kilometer
In km: 8.0 kilometer
Force: 686.70 kilogram * meter / second ** 2
Force in N: 686.70 newton
Caught: Cannot convert from 'kilogram' ([mass]) to 'meter' ([length])
Caught: Cannot convert from 'kilometer / hour' ([length] / [time]) to 'kilogram' ([mass])
When adding 5 km + 3000 m, Pint automatically converts meters to kilometers before summing — you get the right answer without writing any conversion code. And when you try to add kilograms to meters, you get an immediate, descriptive error with the actual dimensions shown. This is the kind of safety that pays off at 2am when you are debugging a calculation that has been silently wrong for months.
Real-Life Example: Unit-Safe Physics Calculator
Here is a physics calculator that uses Pint to ensure all calculations are dimensionally consistent. It computes projectile motion values and never lets a unit slip through without validation:
# physics_calculator.py
from pint import UnitRegistry
import math
ureg = UnitRegistry()
Q_ = ureg.Quantity
def projectile_range(initial_speed, angle_degrees, gravity=None):
"""Calculate the range of a projectile.
Uses the formula: R = (v^2 * sin(2*theta)) / g
All inputs must be Pint Quantities with compatible units.
"""
if gravity is None:
gravity = Q_(9.81, 'm/s^2')
angle_rad = math.radians(angle_degrees)
sin2theta = math.sin(2 * angle_rad)
# Pint tracks units through the calculation
range_distance = (initial_speed ** 2 * sin2theta) / gravity
return range_distance.to(ureg.meter)
def kinetic_energy(mass, velocity):
"""Calculate kinetic energy: E = 0.5 * m * v^2"""
energy = 0.5 * mass * velocity ** 2
return energy.to(ureg.joule)
def fuel_efficiency(distance, fuel_used):
"""Calculate fuel efficiency and convert to common units."""
efficiency = distance / fuel_used
return {
'metric': efficiency.to('km/L'),
'us_mpg': efficiency.to('miles/gallon'),
'uk_mpg': efficiency.to('miles/imperial_gallon'),
}
# Test the calculator
print('=== Projectile Calculator ===')
for angle in [15, 30, 45, 60, 75]:
speed = Q_(50, 'm/s')
r = projectile_range(speed, angle)
print(f'Angle {angle:2d}deg: {r:.1f}')
print('\n=== Kinetic Energy ===')
car_mass = Q_(1500, 'kg')
for speed_kph in [60, 100, 120]:
speed = Q_(speed_kph, 'km/h').to('m/s')
ke = kinetic_energy(car_mass, speed)
print(f'{speed_kph} km/h: {ke:.0f} ({ke.to("kJ"):.1f})')
print('\n=== Fuel Efficiency ===')
trip = Q_(400, 'km')
fuel = Q_(35, 'L')
eff = fuel_efficiency(trip, fuel)
for label, value in eff.items():
print(f' {label}: {value:.2f}')
Output:
=== Projectile Calculator ===
Angle 15deg: 127.4 meter
Angle 30deg: 220.7 meter
Angle 45deg: 254.8 meter
Angle 60deg: 220.7 meter
Angle 75deg: 127.4 meter
=== Kinetic Energy ===
60 km/h: 208333 joule (208.3 kJ)
100 km/h: 578704 joule (578.7 kJ)
120 km/h: 833333 joule (833.3 kJ)
=== Fuel Efficiency ===
metric: 11.43 kilometer / liter
us_mpg: 26.87 mile / gallon
uk_mpg: 32.27 mile / imperial_gallon
The fuel efficiency function is a good example of Pint’s conversion power — the same calculation expressed in liters-per-100-km produces correct US and UK miles-per-gallon values automatically, including the US/UK gallon distinction (the UK imperial gallon is about 20% larger than the US gallon). Without Pint, you would need conversion factors for every pair of units. With Pint, you write the physics once and let the registry handle the rest.
Frequently Asked Questions
How do I install Pint?
Run pip install pint. Pint requires Python 3.8 or newer. For NumPy array support (so you can attach units to entire arrays), NumPy is required but not listed as a mandatory dependency — install it separately with pip install numpy if needed. Pint integrates with NumPy automatically when both are installed.
Should I create one UnitRegistry or multiple?
Create exactly one UnitRegistry per application and reuse it everywhere. Quantities from different registries cannot be combined — even if they have the same units, 5 * ureg1.meter + 3 * ureg2.meter will raise an error because they are considered different registries. The standard pattern is to create a module-level ureg = UnitRegistry() and import it wherever quantities are needed.
Does Pint work with NumPy arrays?
Yes — attach units to NumPy arrays with np.array([1, 2, 3]) * ureg.meter and all NumPy operations (sum, mean, reshape, etc.) preserve the unit. The resulting quantity behaves like a NumPy array with dimensional validation added. This is particularly useful for scientific computing where you work with arrays of measurements and need to ensure consistent units across calculations.
Why does temperature arithmetic behave differently from other units?
Temperature scales like Celsius and Fahrenheit are offset scales — 0 degC does not mean “zero temperature”, it means the freezing point of water. When you convert an absolute temperature (a specific point on the scale), you need to account for the offset. When you compute a temperature difference (how much hotter is A than B), you do not use the offset. Pint distinguishes these with ureg.degC (offset, for absolute temperatures) and ureg.delta_degC (no offset, for differences). Use the correct one for your context.
Can I add custom units to Pint?
Yes — define custom units using ureg.define(). For example: ureg.define('dozen = 12 = doz') adds a “dozen” unit. You can also load custom units from a text file and pass it to the UnitRegistry constructor. Custom units work like built-in units — they participate in conversions, dimensional analysis, and arithmetic with compatible dimensions.
Conclusion
Pint turns physical quantities from raw floats that silently corrupt calculations into validated, self-documenting values that raise descriptive errors when you mix incompatible units. We covered creating quantities with the UnitRegistry, converting between compatible units with .to(), arithmetic with automatic dimensional tracking, catching dimensionality errors at the source, and building a unit-safe physics calculator that handles projectile motion, kinetic energy, and fuel efficiency conversions.
The projectile and fuel calculator project is a template you can apply to any domain — robotics sensor fusion, chemistry calculations, financial unit conversions, or any scientific application where unit safety prevents costly mistakes. Extend it by loading unit definitions from a configuration file, integrating with pandas DataFrames for labeled unit data, or using pint-pandas for unit-aware data analysis.
For the full API reference and advanced features like Babel localization integration, see the official Pint documentation.