Pushing static type checking in Python one step further

As you can see, the errors have been caught before the code went into production, even though our tests — as shown in the example above — were giving us false safety.

Static type checking 102The previous section covered the basic usage of static type checking.

Now let’s see an example of what else we can do with it.

Handling floating-point arithmeticFloating-point numbers are imprecise, which could be a problem when you are working, for example, with money.

Here is a simple example:>>> 100 * 1.

12112.

00000000000001As you can see, the result is not what we would expect.

In Python, this issue can be solved by using Decimal type instead:>>> from decimal import Decimal>>> Decimal("100") * Decimal("1.

12")Decimal("112.

00")That’s better.

However, if float type is passed as an input parameter, the result will be unsatisfactory again:>>> Decimal("100") * Decimal(1.

12) # float passed hereDecimal('112.

0000000000000106581410364')The solution is easy: never pass float numbers as a parameter to Decimal.

You can try to keep enforcing this rule manually and live in fear that you or your colleagues forget about it or do it by accident.

Nevertheless, a better alternative is to enforce this rule automatically by utilising static types.

However, there is still an issue — float is a valid parameter for Decimal.

We need to convince mypy otherwise.

Mypy uses so-called stubs for the definition of types for both standard and third-party libraries.

This way we can define our own stub for Decomal type, which won’t accept the float type as a parameter.

There is an official collection of stubs called typeshed, where we can find a stub for Decimal.

Copy the file into a location where you should keep it for later (in this case a directory custom_typeshed), and prepare to edit it:$ git clone https://github.

com/python/typeshed/$ cp typeshed/stdlib/2and3/decimal.

pyi .

/custom_typeshed/decimal.

pyi$ pico .

/custom_typeshed/decimal.

pyiNow we’ll modify decimal.

pyi to fit our needs:class Decimal(object): # original signature of Decimal.

__new__()- def __new__(cls: Type[_DecimalT], value: _DecimalNew = …, context: Optional[Context] = …) -> _DecimalT: … # our new signature with `value` type restriction+ def __new__(cls: Type[_DecimalT], value: Union[str, int] = …, context: Optional[Context] = …) -> _DecimalT: …The change above means we changed the existing types which can be passed to Decimal, and restricted the accepted values to either str or int only.

To take our stub into account, add the following directive into mypy config (mypy.

ini), where mypy_path value is a path to directory with our modified decimal.

pyi:[mypy]mypy_path = .

/custom_typeshedLet’s prepare a short file example.

py, where we can check everything works as expected:from decimal import Decimalresult = Decimal(100) * Decimal(1.

12) # `float` should not passRunmypywith our config file:$ mypy .

/example.

py –config-file .

/mypy.

iniAnd we should see the following error had been discovered:$ mypy .

/example.

pyexample.

py:2: error: Argument 1 to “Decimal” has incompatible type "float"; expected "Union[str, int]"Now any time we’ll pass an undesired variable type to Decimal, it will be caught automatically, so you can focus on more important things in life instead ^^What’s your experience with type annotations?.Have you found any interesting or unexpected use for them?.Let me know in comments, or join me at Kiwi.

com, so you can tell and show me in person.

.

. More details

Leave a Reply