Search code examples
pythonmetaprogrammingdecoratorpurely-functionalformal-verification

How to check if a function is pure in Python?


A pure function is a function similar to a Mathematical function, where there is no interaction with the "Real world" nor side-effects. From a more practical point of view, it means that a pure function can not:

  • Print or otherwise show a message
  • Be random
  • Depend on system time
  • Change global variables
  • And others

All this limitations make it easier to reason about pure functions than non-pure ones. The majority of the functions should then be pure so that the program can have less bugs.

In languages with a huge type-system like Haskell the reader can know right from the start if a function is or is not pure, making the successive reading easier.

In Python this information may be emulated by a @pure decorator put on top of the function. I would also like that decorator to actually do some validation work. My problem lies in the implementation of such a decorator.

Right now I simply look the source code of the function for buzzwords such as global or random or print and complains if it finds one of them.

import inspect

def pure(function):
    source = inspect.getsource(function)
    for non_pure_indicator in ('random', 'time', 'input', 'print', 'global'):
        if non_pure_indicator in source:
            raise ValueError("The function {} is not pure as it uses `{}`".format(
                function.__name__, non_pure_indicator))
    return function

However it feels like a weird hack, that may or may not work depending on your luck, could you please help me in writing a better decorator?


Solution

  • I kind of see where you are coming from but I don't think this can work. Let's take a simple example:

    def add(a,b):
        return a + b
    

    So this probably looks "pure" to you. But in Python the + here is an arbitrary function which can do anything, just depending on the bindings in force when it is called. So that a + b can have arbitrary side effects.

    But it's even worse than that. Even if this is just doing standard integer + then there's more 'impure' stuff going on.

    The + is creating a new object. Now if you are sure that only the caller has a reference to that new object then there is a sense in which you can think of this as a pure function. But you can't be sure that, during the creation process of that object, no reference to it leaked out.

    For example:

    class RegisteredNumber(int):
    
        numbers = []
    
        def __new__(cls,*args,**kwargs):
            self = int.__new__(cls,*args,**kwargs)
            self.numbers.append(self)
            return self
    
        def __add__(self,other):
            return RegisteredNumber(super().__add__(other))
    
    c = RegisteredNumber(1) + 2
    
    print(RegisteredNumber.numbers)
    

    This will show that the supposedly pure add function has actually changed the state of the RegisteredNumber class. This is not a stupidly contrived example: in my production code base we have classes which track each created instance, for example, to allow access via key.

    The notion of purity just doesn't make much sense in Python.