[Python-ideas] Optional static typing -- late to the party

Andrea Censi censi at mit.edu
Wed Aug 20 14:56:38 CEST 2014


On Wed, Aug 20, 2014 at 5:08 AM, Tennessee Leeuwenburg
<tleeuwenburg at gmail.com> wrote:
>
> I was led to a conclusion: what is wrong with allowing both? Indeed, clearly
> both are actually already supported by various tools. So perhaps there is
> actually a higher-level common concept -- what are the actual assertion
> which are going to be supported? Can I declare a variable only to be a list,
> or can it be a list-of-ints?


Yes - why not both? or all three ways?

Here's the "nonempty list of positive int" example done
using PyContracts [1] in 3 ways:
1) Using decorators;
2) Using annotations (Python 3 only);
3) Using docstrings.

The type "list[length-type](entry-type)" means a list
where the length satisfies length-type and the entries
satisfy entry-type. So "list[>=1](int,>0)" means
a list of length at least 1 whose entries are positive integers.

1) Using decorators:

    @contract(l='list[>=1](int,>0)', returns='int,>0')
    def mysum(l):
        ...

2) Using annotations:

    @contract
    def mysum(l: 'list[>=1](int,>0)') -> 'int,>0':
        ...

3) Using docstrings, with the :type: and :rtype: tags:

    @contract
    def mysum(l):
        """
            :type l: list[>=1](int,>0)
            :rtype: int,>0
        """
        ...

If using (1) and (2), a docstring like in (3) is automatically generated.

[1]: https://pypi.python.org/pypi/PyContracts


Steven D'Aprano says:

> docstring annotations have the disadvantage of being
text only, instead of expressions which evaluate to arbitrarily powerful
or useful objects.

I disagree, it's actually the other way around: Python's general
purpose syntax is actually cumbersome with respect to a custom
language for types.
Once you have the freedom of using a custom language (e.g. using
PyParsing) then you can create very compact and at the same time
powerful contracts.
For example, expressing a type such as "list[>=1](int,>0)" using a
valid Python expression will take much more space.

And you have the ability of creating special syntax where it makes
sense, for example in Numpy --- look at this definition of matrix
multiplication with compatible dimensions:

   @contract
   def matrix_multiply(a,  b):
        ''' Multiplies two matrices together.

            :param a: The first matrix. Must be a 2D array.
            :type a: array[MxN],M>0,N>0

           :param b: The second matrix. Must be of compatible dimensions.
           :type b: array[NxP],P>0

            :rtype: array[MxP]
       '''
        return numpy.dot(a, b)

If the contract is violated, there will be a nice message saying
exactly what's wrong


Example:

    a = numpy.zeros((2,2))
    b = numpy.zeros((3,2))
    matrix_multiply(a,b)

Exception:

    Breach for argument 'b' to matrix_multiply().
    Expected value for 'N' was: Instance of int: 2
            instead I received: Instance of int: 3
    checking: N                for value: Instance of int: 3
    checking: NxP              for value: Instance of tuple: (3, 2)
    checking: array[NxP]       for value: array['3x2'](float64)
array([[ 0.,  0.],        [ 0.,  0.], ... [clip]
    checking: array[NxP],P>0   for value: array['3x2'](float64)
array([[ 0.,  0.],        [ 0.,  0.], ... [clip]


best,
A.

-- 
Andrea Censi | http://censi.mit.edu | "Not all those who wander are lost."
research scientist @ LIDS / Massachusetts Institute of Technology


More information about the Python-ideas mailing list