Mastering the Pint Package in Python for Physical Quantities
Written on
Chapter 1: Introduction to the Pint Package
In fields like engineering, science, and even supply chain management, professionals often find themselves needing to programmatically handle physical quantities such as mass, time, and length. If you're a data scientist or software engineer using Python, you might have resorted to creating lookup tables to convert units (like kg to lb) or to perform operations with different physical dimensions (e.g., volume versus time). The Python ecosystem is rich with libraries that can address these challenges, one of which is Pint. This post will introduce you to Pint, a Python package designed to manage units in your data science or software projects 🍻. You’ll learn about its core features and how to incorporate them into your projects seamlessly 🧩.
Pint Quantity Explained
Pint employs an object-oriented programming (OOP) approach. One of its key components is the Quantity object, which allows you to store both the magnitude and the unit of a physical quantity. Below is a simple illustration of how to create a Quantity instance and access its properties:
from pint import Quantity
medium_q = Quantity("2 kg") # Alternatively, use Quantity((2, "kg"))
print("Magnitude: ", medium_q.m)
print("Magnitude type: ", type(medium_q.m))
print("Dimensionality: ", medium_q.dimensionality)
print("Dimensionality type: ", type(medium_q.dimensionality))
print("Unit: ", medium_q.u)
print("Unit type: ", type(medium_q.u))
Output:
Magnitude: 2
Magnitude type:
Dimensionality: [mass]
Dimensionality type:
Unit: kilogram
Unit type:
The magnitude is stored as an integer (or float), while dimensionality and unit are represented as UnitsContainer and Unit, respectively.
What if your Python backend needs to send this information to other services via RESTful API endpoints? You'll need to serialize these objects for transfer. Here’s how straightforward it is:
dimensionality_dict = dict(medium_q.dimensionality)
unit_string = str(medium_q.u)
print("Dimensionality: ", dimensionality_dict)
print("Dimensionality type: ", type(dimensionality_dict))
print("Unit: ", unit_string)
print("Unit type: ", type(unit_string))
Output:
Dimensionality: {'[mass]': 1}
Dimensionality type:
Unit: kilogram
Unit type:
Now, you have objects that are easily serializable, allowing for display on dashboards or integration into GUIs, as well as for further programming logic like property conversions.
Unit Conversions Made Easy
Let’s explore how simple it is to convert units within the same dimension. The Quantity object has a method called to, which enables you to specify the desired unit for conversion:
print(medium_q.to("ton"))
Output:
0.002204622621848776 ton
Pint also integrates with the uncertainties package, allowing you to manage measurements with tolerance levels related to human error, experimental inaccuracies, or randomness. Tolerance can be presented as absolute (e.g., +/- 0.02 mm) or relative (+/- 0.5%):
print("Absolute: ", medium_q.plus_minus(0.1, relative=False))
print("Relative: ", medium_q.plus_minus(0.1, relative=True))
Output:
(2.00 ± 0.10) kilogram
(2.00 ± 0.20) kilogram
Error propagation is handled effortlessly by Pint, requiring no additional coding on your part:
q_mass = Quantity("2 kg").plus_minus(0.1, relative=True)
q_volume = Quantity("3 m**3").plus_minus(0.1, relative=True)
print(q_mass / q_volume)
Output:
(0.67 ± 0.09) kilogram/meter3
Pint's Unit Registry
Pint allows for the creation of a UnitRegistry object to isolate the unit systems your application will utilize. From an OOP perspective, it employs a composite design pattern, primarily involving Quantity and Unit.
from pint import UnitRegistry
medium_ureg = UnitRegistry()
medium_q = medium_ureg.Quantity("2 kg").plus_minus(0.1, relative=True)
print(medium_q)
Output:
2.00+/-0.20 kilogram
Serialization of Quantity Instances
Quantity instances can be serialized and transformed into tuples using the to_tuple() method, and you can convert them back into Quantity instances via the from_tuple() method of the UnitRegistry class:
medium_q_2 = medium_ureg.Quantity.from_tuple(medium_q.to_tuple())
print(medium_q_2)
Output:
2.00+/-0.20 kilogram
Python tuples are pickable, meaning you can easily save and load Quantity object attributes using these methods:
import pickle
medium_q_tuple = pickle.loads(pickle.dumps(medium_q.to_tuple()))
new_medium_q = medium_ureg.Quantity.from_tuple(medium_q_tuple)
print(new_medium_q)
Output:
2.00+/-0.20 kilogram
Handling Multiple Unit Registries
If you mistakenly use a Quantity instance from a different registry, Pint will raise an error:
Quantity("2 kg") / medium_ureg.Quantity("5 m**3")
Output:
ValueError: Cannot operate with Quantity and Quantity of different registries.
As previously mentioned, Quantity is part of the UnitRegistry class, so mixing units from distinct registries will result in a crash 🙈.
Integrating with Python Lists and Numpy Arrays
Pint extends its functionality to Python lists and NumPy arrays. Here’s how easy it is to specify units for physical quantities in a list:
medium_q_list = [0.5, 0.4] * medium_ureg.kg
print(medium_q_list.to("g"))
Output:
[500.0 400.0] gram
You can also use nested lists to represent different physical properties:
medium_q_list = [[0.3, 0.2] * medium_ureg.kg,
[0.3, 0.2] * medium_ureg.J]
print(medium_q_list)
For NumPy arrays, Pint allows you to define separate arrays with their own dimensions and units, enabling standard NumPy operations like element-wise multiplication and mean calculations 🦾.
import numpy as np
import warnings
warnings.simplefilter("ignore")
medium_mass = np.array([0.4, 0.2]) * medium_ureg.kilogram
medium_density = np.array([0.4, 0.2]) * medium_ureg.Unit("kg/m**3")
medium_volume = medium_mass / medium_density
print(medium_volume)
print(medium_mass.mean())
Output:
[1.0 1.0] meter ** 3
0.30000000000000004 kilogram
Unlike Python lists, NumPy arrays cannot express different units for columns or rows, which may be limiting if your data structure requires it. However, Pint continues to evolve, integrating its features with frameworks like Pandas and Xarray 🚀🌝.
Defining and Redefining Units
One of the standout features of Pint is its capability to define new dimensions and units, as well as to redefine existing unit conversions.
New Unit Definition
For instance, if you want to define a new unit within the mass dimension, you could create a unit called medium_silly with aliases msu and msum, where 0.2 kg equals 1 medium_silly:
medium_ureg.define("medium_silly = 0.2 * kg = msu = msum")
This newly defined unit can be utilized for calculations:
new_quantity = (1 * medium_ureg.medium_silly +
1 * medium_ureg.msu +
1 * medium_ureg.msum +
1 * medium_ureg.kg)
print(new_quantity)
Output:
8.0 medium_silly
New Dimension Definition
You can also introduce a new dimension into the registry, for example, silly_dim, referencing the medium_silly_dim_ref unit:
medium_ureg.define("medium_silly_dim_ref = [silly_dim] = msd_ref")
print(medium_ureg.msd_ref.dimensionality)
Output:
[silly_dim]
Existing Unit Redefinition
To redefine existing units, you need to use the Context object to specify the context for the redefinition. Here’s how:
from pint import Context
# Define context
ctx = Context(name="Medium test")
medium_ureg.add_context(ctx)
# Redefine unit
ctx.redefine("BTU = 1055 J")
# Create unit and convert it
q = medium_ureg.Quantity("1 BTU")
print(q.to("J"))
Output:
1055.056 joule
The result can differ if the context isn’t specified during the conversion:
print(q.to("J", ctx))
Output:
1055.0 joule
Programmatic Definitions
Pint stores unit definitions in a text file named default_en.txt located in its installation path. You can modify this file or create a new one to load custom definitions:
medium_ureg.load_definitions("/your/path/to/customized_def.txt")
While modifying this file programmatically may be resource-intensive, it’s essential to understand how to leverage the Pint ecosystem effectively.
Conclusion
In this article, we've delved into the Pint package for managing physical quantities in Python projects. We've covered core components of the OOP paradigm that underpin Pint, such as Quantity, Unit, UnitRegistry, and Context. Additionally, we've explored how to define and redefine unit conversions and dimensions in your application. In the next post, I'll share strategies for overcoming challenges associated with storing programmatically defined units. Let's continue this journey of exploration and learning together 📖. Thank you for reading my post 🤗.
If you enjoyed this content, consider following me on Medium for more insightful articles 🚀…
The first video titled "Using Pints for Units in Python" provides a comprehensive overview of how to effectively use Pint for unit handling in Python.
The second video, "Python Unit Conversions with Pint," dives deeper into unit conversions and practical applications of the Pint package.