When you need to perform calculations with values that have an associated unit of measure, it is very common to make mistakes by mixing different units of measure. It is also common to perform incorrect conversions between the different units that generate wrong results. The latest Python release doesn't allow developers to associate a specific numerical value with a unit of measure. In this article, I look at three Python packages that provide different solutions to this problem and allow you to work with units of measure and perform conversions.
Three Different Packages to Add Units of Measure
The need to associate quantities with units of measure in any programming language is easy to understand, even in the most basic math and physics problems. One of the simplest calculations is to sum two values that have an associated base unit. For example, say that you have two electrical resistance values. One of the values is measured in ohms and the other in kilo-ohms. To sum the values, you must choose the desired unit and convert one of the values to the chosen unit. If you want the result to be expressed in ohms, you must convert the value in kilo-ohms to ohms, sum the two values expressed in ohms, and provide the result in ohms.
The following Python code uses variables with a suffix that defines the specific unit being used in each case. You have probably used or seen similar conventions. The suffixes make the code less error-prone because you easily understand that
r1_in_ohms holds a value in ohms, and
r2_in_kohms holds a value in kilo-ohms. Thus, there is a line that assigns the result of converting the
r2_in_kohms value to ohms to the new
r2_in_ohms variable. The last line calculates the sum and holds the result in ohms because both variables hold values in the same unit of measure.
r1_in_ohms = 500 r2_in_kohms = 5.2 r2_in_ohms = r2_in_kohms * 1e3 r1_plus_r2_in_ohms = r1_in_ohms + r2_in_ohms
Obviously, the code is still error-prone because there won't be any exception or syntax error if a developer adds the following line to sum ohms and kilo-ohms without performing the necessary conversions:
r3_in_ohms = r1_in_ohms + r2_in_kohms
There is no rule that assures that all the variables included in the
sum operation must use the same suffix; that is, the same unit. There aren't invalid operations between variables that hold values with incompatible units. For example, you might sum a
voltage value to a
resistance value and the code won't produce any error warning.
Now, imagine that Python adds support for units of measure. Each numeric value can have an associated unit of measure enclosed within <>. The following three lines would replace the previous code with an easier to understand syntax:
r1 = 500 <ohms> r2 = 5.2 <kilo-ohms> r1_plus_r2 = (r1 + r2) <ohms>
r1 holds a value of
500 and an associated unit of measure,
r2 holds a value of
5.2 and an associated unit of measure,
kilo-ohms. Because each variable includes information about its unit of measure, the
sum operation is smart and it can convert compatible units such as
kilo-ohms. The last line sums the two values taking into account their unit of measure, and converts the result to the specified unit,
r1_plus_r2 variable holds the result of the
sum operation expressed in
ohms. The following line would produce an exception because the units of measure are incompatible:
sum = (10 <volts> + 500 <ohms>) <inches>
However, the support should be smart enough to allow you to mix different length units. For example, the following line would produce a valid result in inches.
sum = (10 <inches> + 1200 <centimeters>) <inches>
Python doesn't support units of measure, but the three Python packages I examine here provide different ways to enable them. Each package takes a different approach. While none works as well as a native language feature would, these solutions do provide a baseline that you can improve according to your specific needs.
Python Package Web Page
Numericalunits: A Bare-Bones Solution
Numericalunits is a single Python module (numericalunits.py) that provides easy unit conversion features for your operations. You just need to follow two simple rules:
- To assign a unit of measure to a value, multiply the value by the unit.
- To express a value generated by its multiplication by the unit in a different unit, divide the value by that unit.
You simply need to add the following lines to import the module with an alias (
nu) and execute the $
reset_units method to start working with the different units.
import numericalunits as nu nu.reset_units('SI')
When you call
nu.reset_units('SI'), Numericalunits uses standard SI units (short for Système Internationale d'Unités in French) for storing the values (see Figure 1). This way, any length value is stored in meters, no matter the length unit you specify in the multiplication. Read here if you want more information about SI base units.
Figure 1: The SI base units and their interdependencies.
If you call the default
nu.reset_units(), Numericalunits uses a random set of units instead of the standard SI units. I really don't like using a random set of units because it usually generates a loss of precision and results that lack accuracy. The only advantage of using random units is that you can check dimensional errors by running calculations twice and comparing whether the results match. You have to call
nu.reset_units() before each calculation and compare the two values. I don't like this way of checking dimension errors because it adds a huge overhead and it is indeed error-prone. Thus, I suggest using Numericalunits as a unit conversion helper with the standard SI units initialization.
Numericalunits doesn't save information about the unit of measure in the numerical variable; therefore, there is no way to know which unit you used when you assigned the value. If you need more than a unit conversions helper, I suggest working with one of the other packages.
You can read the following line of code as "assign 500 ohms to
r1 = 500 * nu.ohm
You can read the following line of code as "display the value of
r1 expressed in kilo-ohms."
print(r1 / nu.kohm)
The following line displays the value of
r1 expressed in ohms.
print(r1 / nu.ohm)
The following code uses Numericalunits to sum the values of
r2. Notice that the code is self-documented because you can easily see that
r1 holds a value in
r1_plus_r2 variable holds the result of the
sum operation expressed in
r1_plus_r2_kohms holds the result converted to
kilo-ohms. Notice that you can sum the values of
r2 without having to convert the units to ohms and the result will be accurate because of the way in which Numericalunits saves the values in the base units.
import numericalunits as nu nu.reset_units('SI') r1 = 500 * nu.ohm r2 = 5.2 * nu.kohm r1_plus_r2 = r1 + r2 r1_plus_r2_kohms = r1_plus_r2 / nu.kohm
The following code uses Numericalunits to sum four
distance values expressed in four different units of measure: meters, miles, centimeters, and feet. Numericalunits doesn't support plurals for the units. The
total_distance variable holds the total distance expressed in feet.
import numericalunits as nu nu.reset_units('SI') distance1 = 2500 * nu.m distance2 = 2 * nu.mile distance3 = 3000 * nu.cm distance4 = 3500 * nu.foot total_distance = (distance1 + distance2 + distance3 + distance4) / nu.foot
Pint: A Complete Package
Pint is a Python package that allows you to work with numerical values associated to units of measure. Because Pint saves the magnitude and the associated units for any numerical type, you are able to know which unit you used when you assigned the magnitude value. You can perform arithmetic operations between compatible units and convert from and to different units. When you try to perform arithmetic operations on magnitudes that have incompatible units of measure, Pint raises a
Pint.unit.DimensionalityError exception indicating that it cannot convert from one unit to the other before performing the arithmetic operation.
UnitRegistry class stores the definitions and relationships between units. By default, Pint uses the
default_en.txt unit definitions file. This file contains the different units and the prefixes that the
UnitRegistry will recognize in plain text. You can easily edit this text file to add any unit you might need to support.