Search code examples
pythonintegerlimitboundarybounded-types

How can I create a bounded int in Python?


I want to create a class that subclasses from int, but sets a lower bound and upper bound on what number it can be be.

For example, if the lower bound is 2, then a = MyClass(1) should raise an exception.

I'm struggling because int doesn't seem to have an __init__ function, so I'm not sure how to subclass from it, and my attempts are giving me errors.

How should I go about doing this?


Solution

  • Try this. It should work both for ints and floats:

    def BoundedNumber(number_class):
        def BoundedNumberClassCreator(class_name, lower_bound, upper_bound):
            if upper_bound and lower_bound and upper_bound < lower_bound:
                raise ValueError(f"Upper bound {upper_bound} is lower than the lower bound {lower_bound}")
    
            def new(cls, number):
                if lower_bound and number < lower_bound:
                    raise ValueError(f"{number} is below the lower bound of {lower_bound} for this class")
    
                if upper_bound and upper_bound < number:
                    raise ValueError(f"{number} is above the upper bound of {upper_bound} for this class")
    
                return number_class(number)
    
            return type(class_name, (number_class,),
                        {"__new__": new,
                         "__doc__": f"Class that acts like `{number_class.__name__}` but has an inclusive lower bound of {lower_bound} and an inclusive upper bound of {upper_bound}",
                         "lower_bound": lower_bound,
                         "upper_bound": upper_bound})
    
        return BoundedNumberClassCreator
    
    BoundedInt = BoundedNumber(int)
    BoundedFloat = BoundedNumber(float)
    
    if __name__ == "__main__":
        IntBetween50And150 = BoundedInt('IntBetween50And150', 50, 150)
        print(IntBetween50And150(100) == 100)  # True
        try:
            IntBetween50And150(200)
        except ValueError as e:
            print(f"Caught the ValueError: {e}")  # Caught the value error: 200 is above the upper bound of 150 for this class
    
        print(IntBetween50And150(50.5))  # 50
        print(IntBetween50And150.__doc__) # Class that acts like `int` but has an inclusive lower bound of 50 and an inclusive upper bound of 150
    

    The hard part with subclassing from int is that it doesn't have an __init__ function. Instead, you have to use the __new__ function.

    The BoundedNumber class takes care of this, defining a __new__ function that both runs the int (or float) __new__ function by calling int (or float), but also runs its own checks on the bounds before doing so.

    Since we want to dynamically create a new class, we're going to have to use the type function. This will allow us to create a new class with whatever bounds we want during runtime.

    Technically, to answer your question, you only need the BoundedNumberClassCreator with int put in everywhere that the number_class is used, but since it works for floats as well, I figured I'd encapsulate it to reduce duplicate code.

    One odd thing about this solution if if you ZeroToOne = BoundedInt('ZeroToOne', 0, 1) and then create i = ZeroToOne(1.1) it will throw an error, even though int(1.1) is within the designated range. If you don't like this functionality, you can swap the order of the checks and the return inside of the new method of the BoundedNumberClassCreator.