pint
pint copied to clipboard
Typing in 0.22
This issue is to address typing related topics that we should for 0.22. The objective is not to fix every typing aspects but those exposed to the users
Now that I have refactored the code, I have a lot of doubts in relation to make Quantity Generic over the magnitude. I think it complicates things, and we get little in return: mainly to flag something as a scalar or array quantity, but I am not sure if it works well. Something to consider, maybe not to change now but for the future,
For my use case (sustainable finance and climate change), the use case for typing in Pint is to ensure that certain dimensions are present. For example, Emissions should be an amount of GHG gas (typically tons of CO2e). Emissions intensity should be an amount of GHG gas per unit of Production (and there are many units of production--tons of Steel, tons of Cement, square meters of buildings, petajoules, barrels of oil, therms, cubic meters of natural gas, etc, so that's like tons of CO2e per Generic). Monetary quantities should be in some form of currency (which, because of floating rates are easily typed--but not easily converted--at parsing time).
Hi! I am trying to figure out how to type hint with pint and I haven't found a solution yet. I tried some of the stuff from here but I doesn't seem to work. My use case is as follows:
I made some code that has functions that take a value and a unit. Some signatures here as an example:
import numpy as np
def func_scalar(x: float, unit: str) -> float:
pass
def func_array(x: np.ndarray[(3, ), int], unit: str) -> np.ndarray[(3, ), int]:
pass
But then I discovered pint and I though "that's amazing! I should just use pint!". I hoped I could do something like this:
import numpy as np
import pint
def func_scalar(x: pint.Quantity[float]) -> pint.Quantity[float]:
pass
def func_array(x: pint.Quantity[np.ndarray[(3, ), int]]) -> pint.Quantity[np.ndarray[(3, ), int]]:
pass
But this results in the TypeError: <class 'pint.registry.Quantity'> is not a generic class
. As I understand from reading this issue, Quantity
has not been made a generic class yet. And maybe it will never be.
So, my question is: is there currently a way to achieve something like my goal?
Hi guys. Sharing my solution in case anybody else searching for a solution to this bumps into this thread.
The solution I ended up with is to use Annotated
. For example:
import pint
import numpy as np
from typing import Annotated, Any
# A function to compute the Sound Pressure Level (SPL) of a pressure time series.
def spl(
pressure: Annotated[pint.Quantity, np.ndarray[(Any, ), float]],
reference = Annotated[pint.Quantity, float]
) -> Annotated[pint.Quantity, np.ndarray[(Any, ), float]]:
return ((np.sqrt(np.nanmean(pressure**2)) / reference)**2).to('decibel')
It doesn't give the best autocompletion in PyCharm (which is something I enjoy about type hints) but for now It might be good enough for my purposes.
xref https://github.com/pydantic/pydantic/discussions/4929
@hgrecco: Please add typing
label to this issue.
Here's my attempt to create a semi-generic Quantity type that can be used to build class structures containing specific Quantity types using Pydantic 2.x:
from typing import Any
from pydantic_core import CoreSchema, core_schema
from pydantic import BaseModel, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
import pint
ureg = pint.UnitRegistry()
from pint import Quantity
from pint import Quantity as Q_
def to_Quantity(quantity: Any) -> Quantity:
if isinstance(quantity, str):
try:
v, u = quantity.split(' ', 1)
if v == 'nan' or '.' in v or 'e' in v:
quantity = Q_(float(v), u)
else:
quantity = Q_(int(v), u)
except ValueError:
return ureg(quantity)
elif not isinstance(quantity, Quantity):
raise ValueError(f"{quantity} is not a Quantity")
return quantity
def Quantity_type(units: str) -> type:
"""A method for making a pydantic compliant Pint quantity field type."""
def validate(value, units, info):
quantity = to_Quantity(value)
assert quantity.is_compatible_with(units), f"Units of {value} incompatible with {units}"
return quantity
def __get_pydantic_core_schema__(
source_type: Any
) -> CoreSchema:
return core_schema.general_plain_validator_function(
lambda value, info: validate(value, units, info)
)
@classmethod
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
json_schema = handler(core_schema)
json_schema = handler.resolve_ref_schema(json_schema)
json_schema['$ref'] = "#/definitions/Quantity"
return json_schema
return type(
"Quantity",
(Quantity,),
dict(
__get_pydantic_core_schema__=__get_pydantic_core_schema__,
__get_pydantic_json_schema__=__get_pydantic_json_schema__,
),
)
class Foo(BaseModel):
field_int: int
field_m: Quantity_type('m')
field_s: Quantity_type('s')
class Bar:
foo = Foo(
field_int = 1,
field_m='1 m',
field_s=Q_(1, 's')
)
xx = Bar()
print("=================")
print(f"xx.foo = {xx.foo}")
print("=================")
class Baz:
foo = Foo(
field_int = 2,
field_m='1 gal',
field_s=Q_(1, 'kg')
)
yy = Baz()
print("=================")
print(f"yy.foo = {yy.foo}")
print("=================")
Just as a side note, the solution proposed by @stefano-tronci will only work for Python version >= 3.9 as typing.Annotated
was introduced in Python 3.9.
I'm also trying to find with a solution that combines runtime dimensionality validation with static type safety. Sadly none of the solutions shared here or at https://github.com/hgrecco/pint/issues/1166 typechecks statically (at least with mypy).
The most promising direction I've found so far is something like the InstanceOf Pydantic validator that makes InstanceOf[Foo]
appear like Foo
to mypy but like Annotated[Foo, InstanceOf()]
to Pydantic.
Applying this idea to pint, I tried to come up with a TypedQuantity
class so that TypedQuantity['[length]']
appears like pint.Quantity
to mypy and an Annotated
to Pydantic. Mypy does not handle __class_getitem__
as of now so it doesn't accept strings as type items. Wrapping the string '[length]' to a Length
class worked around this issue (by fooling mypy to think of TypedQuantity[Length]
as generic) but made the annotation equivalent to Length
instead of pint.Quantity
. That's as far as I got 🤷♂️