Using & , | and ~ actually a pretty good option. You just need to document that parentheses are necessary because of the different operator precedence.
SQLAlchemy does it like this, for example. For people who do not like this kind of abuse of bitwise operators, it also provides functions and_(*args) , or_(*args) and not_(arg) , which perform the same thing as their counterparts of operators. However, you are forced to prefix the notation ( and_(foo, bar) ), which is not as readable as the infix notation ( foo & bar ).
The lambda approach is also a good idea (besides the ugliness introduced by lambda itself). Unfortunately, AST is really not available without source code, but wait, you have source code that is simply not tied to a function object!
Imagine this code:
import ast import inspect def evaluate(constraint): print ast.dump(ast.parse(inspect.getsource(constraint))) evaluate(lambda x: x < 5 and x > -5)
This will give you this AST:
Module( body=[ Expr( value=Call( func=Name(id='evaluate', ctx=Load()), args=[ Lambda( args=arguments( args=[ Name(id='x', ctx=Param()) ], vararg=None, kwarg=None, defaults=[] ), body=BoolOp( op=And(), values=[ Compare( left=Name(id='x', ctx=Load()), ops=[Lt()], comparators=[Num(n=5)] ), Compare( left=Name(id='x', ctx=Load()), ops=[Gt()], comparators=[Num(n=-5)] ) ] ) ) ], keywords=[], starargs=None, kwargs=None ) ) ] )
The disadvantage is that you get the whole source string - but you can easily go through the AST until you reach the lambda expression (the first in the call of your evaluation function), and then you can only work with the corresponding part.
To avoid having to evaluate it yourself, now you can simply rewrite the AST to use bitwise operators instead, and then compile the new AST for a function that will then use overloaded operators.
Let's look at AST ((x < 5) & (x > -5)) :
body=BinOp( left=Compare( left=Name(id='x', ctx=Load()), ops=[Lt()], comparators=[Num(n=5)] ), op=BitAnd(), right=Compare( left=Name(id='x', ctx=Load()), ops=[Gt()], comparators=[Num(n=-5)] ) )
As you can see, the difference is pretty slight. You just need to rewrite your BUTOp to use BinOp!
AST and_(x < 5, x > -5) will look like this:
body=Call( func=Name(id='and_', ctx=Load()), args=[ Compare( left=Name(id='x', ctx=Load()), ops=[Lt()], comparators=[Num(n=5)] ), Compare( left=Name(id='x', ctx=Load()), ops=[Gt()], comparators=[Num(n=-5)] ) ], keywords=[], starargs=None, kwargs=None )
It is also not too difficult to rewrite.