unit-syntax adds support for physical units to the Python language:
>>> speed = 5 meters/second
>>> (2 seconds) * speed
10 meter
Why? I like to use Python as a calculator for physical problems and wished it had the type safety of explicit units along with the readability of normal notation.
unit-syntax works in Jupyter notebooks, standalone Python scripts, and Python packages.
Getting Started
Install the package:
$ pip install unit-syntax
... with Jupyter/IPython
To enable unit-syntax in a Jupyter/IPython session run:
%load_ext unit_syntax
Tip: In Jupyter this must be run in its own cell before any units expressions are evaluated.
... with standalone scripts
To run a standalone script with units:
$ python -m unit_syntax <path_to_script.py>
Note that this installs a custom import hook that affects all imports performed by the script.
... with Python packages
To use/distribute a package with unit-syntax, add this in your __init__.py:
from unit_syntax.import_hook import enable_units_for_package
enable_units_for_package(__name__)
This applies the transform only to sub-modules of your package.
Usage
An interactive notebook to play around with units
Units can be applied to any "simple" expression:
- number:
1 meter - variables:
x parsec,y.z watts,area[id] meters**2 - lists and tuples:
[1., 37.] newton meters - unary operators:
-x dBm - power:
x**2 meters
In expressions mixing units and binary operators, parenthesize:
one_lux = (1 lumen)/(1 meter**2)
Units can be used in any place where Python allows expressions, e.g:
- function arguments:
area_of_circle(radius=1 meter) - list comprehensions:
[x meters for x in range(10)]
Quantities can be converted to another measurement system:
>>> (88 miles / hour) furlongs / fortnight
236543.5269120001 furlong / fortnight
>>> (0 degC) degF
31.999999999999936 degree_Fahrenheit
Compound units (e.g. newtons/meter**2) are supported and follow the usual precedence rules.
Units may not begin with parentheses (consider the possible
interpretations of x (meters)). Parentheses are allowed anywhere else:
# parsed as a function call, will result in a runtime error
x (newton meters)/(second*kg)
# a-ok
x newton meters/(second*kg)
Using unknown units produces a syntax error at import time:
>>> 1 smoot
...
SyntaxError: 'smoot' is not defined in the unit registry
How does it work?
unit-syntax transforms python-with-units into standard python that calls the excellent pint units handling library.
The parser is pegen, which is a standalone version of the same parser generator used by Python itself. The grammar is a lightly modified version the official Python grammar shipped with pegen.
Syntax transformation in IPython/Jupyter uses IPython custom input transformers.
Syntax transformation of arbitrary Python modules uses importlib's MetaPathFinder, see import-transforms and unit_syntax.import_hook for details.
Why only allow units on simple expressions?
Imagine units were instead parsed as operator with high precedence and you wrote this reasonable looking expression:
ppi = 300 pixels/inch
y = x inches * ppi
inches * ppi would be parsed as the unit, leading to (at best) a runtime error sometime later and at worst an incorrect calculation. This could be avoided by parenthesizing the expression (e.g. (x inches) * ppi, but if that's optional it's easy to forget. So the intent of this restriction is to make these risky forms uncommon and thus more obvious. This is not a hypothetical concern, I hit this within 10 minutes of first using the initial syntax.
Prior Art
The immediate inspriration of unit-syntax is a language called Fortress from Sun Microsystems. Fortress was intended as a modern Fortran, and had first-class support for units in both the syntax and type system.
F# (an OCaml derivative from Microsoft) also has first class support for units.
The Julia package Unitful.jl
A long discussion on the python-ideas mailing list about literal units in Python.
Development
To regenerate the parser:
python -m pegen python_units.gram -o unit_syntax/parser.py
Running tests:
$ poetry install --with dev
$ poetry run pytest
Future work and open questions
- Test against various ipython and python versions
- Ensure bytecode caching still works
- Test with wider range of source files with the wildcard loader
- Unit type hints, maybe checked with @runtime_checkable. More Pint typechecking discussion
- Typography of output
- pre-parse units
- talk to pint about interop between UnitRegistries
- Fix reported location of SyntaxError when paren missing
Metadata
Release files for unit-syntax 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| unit_syntax-0.3.1.tar.gz | 36.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| unit_syntax-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 72.5 kB
Release files / unit_syntax-0.3.1.tar.gz
| Download URL | unit_syntax-0.3.1.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b6825b4cc01ebd60cc97866294d8f8ec6a941dea1b360908c0eefc8c1a825e6f
|
|
BLAKE2b-256 checksum How to use checksums |
7031d0873c6726ef4c34950866643e5b22b9ed261506f75e7acc8f453a7e4b36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/4.0.2 CPython/3.11.4
|
Release files / unit_syntax-0.3.1-py3-none-any.whl
| Download URL | unit_syntax-0.3.1-py3-none-any.whl |
|---|---|
| Size | 35.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64911364c098be7dbaeb15c51ff8b5a228ee333a6227a4f7854d433cc4a85458
|
|
BLAKE2b-256 checksum How to use checksums |
e1cba329c1dc89ff1a3fdf5a450b444c5ca1de40767179c3d08670dfc64bbbab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/4.0.2 CPython/3.11.4
|