1

The following code prints '1 dimensionless':

import pint
ureg=pint.UnitRegistry()
print(ureg(0.))

Why, Pint?

Ethan Keller
  • 963
  • 3
  • 10
  • 26

2 Answers2

2

"Calling" a UnitRegistry object is equivalent to calling parse_expression on it. parse_expression expects to receive a str, and it has a special case for the empty string, which is to return it as a Quantity(1) (the 1 dimensionless you see).

In this case, you happened to hit a minor flaw in the duck-typing: It expects a string, but doesn't actually verify that it received one. Then it converts any falsy value to Quantity(1) with the code:

if not input_string:
    return self.Quantity(1)

so any zero-valued number (or None, or empty sequence, or other falsy thing) becomes Quantity(1). Had you passed it a truthy expression of an unexpected type, the parser would have gotten involved and raised an exception, but falsy values never even reach the parser.

I'm not clear on why the empty expression should be a Quantity(1), but the authors explicitly put that check in there, so it must have been intended.

In short, don't pass non-strings to the function. They'll fail silently when falsy, and raise an exception for anything else (when it assumes they're a str and it tries to call str methods on them).

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
2

looks like a bug/limitation in the package.

When passing an integer (different from 0) pint crashes:

>>> ureg(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python34\lib\site-packages\pint\registry.py", line 837, in parse_expression
    input_string = string_preprocessor(input_string)
  File "C:\Python34\lib\site-packages\pint\util.py", line 579, in string_preprocessor
    input_string = input_string.replace(",", "")
AttributeError: 'int' object has no attribute 'replace'

in registry.py

def parse_expression(self, input_string, case_sensitive=True, **values):
    """Parse a mathematical expression including units and return a quantity object.

    Numerical constants can be specified as keyword arguments and will take precedence
    over the names defined in the registry.
    """

    if not input_string:
        return self.Quantity(1)

    input_string = string_preprocessor(input_string)  # should not be called with something else than string

it crashes because the package tries to perform string operations on the non-string, where a string is expected. But the test is if not input_string so 0.0 make pint create a 1 class (or whatever that means), just like if you'd pass "". Passing 1 allows to reach the next line, which crashes.

It's just missing a type check, something like:

    if not isinstance(input_string,str):
        raise Exception("a string is required")
Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • While your explanation is mostly correct, `_from_string` isn't involved in this at all. `parse_expression` is what gets called, and it has a similar bug in its input testing, so you're almost correct, but it's a completely different code path. – ShadowRanger Oct 05 '18 at 19:31
  • my traceback indicates `string_preprocessor`, though. Different version? – Jean-François Fabre Oct 05 '18 at 19:32
  • `string_preprocessor` is called from `parse_expression`, it's just that `parse_expression` is in a different module entirely. – ShadowRanger Oct 05 '18 at 19:32
  • Right, but where is `_from_string` in the trace? Your traceback never mentions it at all (while it does mention `parse_expression`). – ShadowRanger Oct 05 '18 at 19:33