1

I'd like to make a class that extends the int base class, so that the object itself is an integer (i.e. you set it and read from it directly), but also has input validation - for example, only allow a given range.

From what I have researched, the __init__ method is not called when you extend normal base classes, so I'm not sure how to do this. I'm also not clear on how you access the value of the object (i.e. the actual integer assigned to it) or modify that value from within the class. I see the same issue extending any base class (string, float, tuple, list, etc.).

If I could use __init__ it would be something like:

class MyInt(int):
    def __init__(self, value):
        if value < 0 or value > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(value))
        self = value

How can I validate new values coming into my extended int class?

martineau
  • 119,623
  • 25
  • 170
  • 301
LightCC
  • 9,804
  • 5
  • 52
  • 92
  • I think you can only achieve this from the `__new__` method, something like this answer should work. https://stackoverflow.com/a/46196226/1284043 What's the usecase, do you really need to subclass int? – MarkM May 09 '20 at 16:17
  • @MarkM I'm still learning, so maybe I don't need to subclass int. I've run across several cases where I think using extended base classes would simplify a lot of things, since you treat it directly as an int (or string, tuple, or whatever), but you can still have a setter function, and still even add some custom methods. Thanks for the link to `__new__`, I'll check that out. – LightCC May 09 '20 at 18:22
  • Isn't it simpler and safer to leave validation for external code? Validate your data, and only create objects from valid data. – progmatico May 10 '20 at 19:05
  • @progmatico If the "MyInt" were being used as a library in multiple locations, with the same validation needed for any implementation, then that would cause code duplication. It's the right question to ask though. – LightCC May 10 '20 at 20:11
  • 1
    That is a good point. But consider also two things. You can group validation code somewhere and always call for validations from there, and you do not have to validate at every place. You trust yourself, and you can assert if you don't. Where you need validation is at data acquisition points, and that is probably made in very few points in the code. – progmatico May 11 '20 at 17:02

3 Answers3

2

You don't actually have to override __new__ in this case. After the object is created, __init__ will be called, and you can check if it is in range or not.

class MyInt(int):
    def __init__(self, x, **kwargs):
        if self < 0 or self > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(x))

You can override __new__ to raise the exception before MyInt(...) returns the new object.

class MyInt(int):
    def __new__(cls, *args, **kwargs):
        x = super().__new__(cls, *args, **kwargs)  # Let any value error propagate
        if x < 0 or x > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(x))
        return x

You might want to try to validate the argument before calling super().__new__, but strictly speaking, that's not playing well with other classes.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • @chapner I thought __init__ was not run when subclassing from base types like int? – LightCC May 09 '20 at 22:40
  • @LightCC `int.__init__` isn't defined; if you try to call it, it just resolves to `object.__init__`, which doesn't really do anything. If you define `__init__` on your subclass, it will still be called. – chepner May 09 '20 at 23:43
  • Also, keep that in mind if you call `super().__init__` from inside `MyClass.__init__`; the arguments passed to `int.__new__` won't be expected by `object.__init__` if you try to pass arguments to `super().__init__`. – chepner May 09 '20 at 23:53
  • @chapner Okay, that makes sense, but in this case it won't resolve the issue. At the point of the init the object has already been created, and it is immutable, so the value cannot be adjusted or changed. I suppose you're pointing out that if all you want to do is raise an exception, that can be done... By the way, the info on object.__init__ makes sense, I wasn't thinking of the full hierarchy beyond int. – LightCC May 10 '20 at 02:45
  • Correction to above answer: in the ValueError, the variable is `x` not `value` – Ashton Honnecke Jun 03 '22 at 18:59
0

After researching on the __new__ function from MarkM's suggested link, that approach will do what I'm looking for, but I'm also questioning if it's the right approach. See both the discussion and solution sections following.


Discussion about subclassing base immutable tyeps

However, I'm also questioning the need to do this, at least for simple input validation checks. The nice thing about subclassing an immutable base class is that you get all the benefits of immutability, like built-in hashing, string representation, etc., that just inherits directly from the base class. But you would get the same benefits by simply adding a property to another class, add a setter function, and make it immutable.

The advantage with the subclassing is if you want immutable objects that all have the same input validation and/or additional methods that can be applied to many different classes/modules, without the additional complexity of creating a separate class which converts them into mutable objects, and then requires an additional level of property or method access (i.e. you make a "myint" object from a "MyInt" Class, but need a "value" property or something similar to access the underlying int, such as myint.value rather than just myint).


Solution / Implementation

So, there may be a better way to do this, but to answer my own question, here is a test script I wrote to at least get this working.

Note that int can have multiple arguments, when interpreting a string into a specific base, such as ('1111', 2), which converts the binary string '1111' to decimal 15. The base can be entered as a kwarg as well, so passing *args and **kwargs on in full to the int __new__ function is required.

Also, I ended up making the call to int first, before doing validation, so that it would convert floats and strings to int first, before attempting validation.

Note that since MyInt subclasses int, you must return an int value - you can't return a "None" on failure (though you could return a 0 or -1). This led me to raise a ValueError and handle errors back in the main script.

class MyInt(int):

    def __new__(cls, *args, **kwargs):
        value = super().__new__(cls, *args, **kwargs)
        argstr = ', '.join([str(arg) for arg in args]) # Only for prototype test
        print('MyInt({}); Returned {}'.format(argstr, value)) # Only for prototype test
        if value < 0 or value > 100:
            raise ValueError('  ERROR!! Out of range! Must be 0-100, was {}'.format(value))
        return value


if __name__ == '__main__':
    myint = MyInt('1111', 2)  # Test conversion from binary string, base 2
    print('myint = {} (type: {})\n'.format(myint, type(myint)))

    for index, arg in enumerate([-99, -0.01, 0, 0.01, 0.5, 0.99, 1.5, 100, 100.1, '101']):
        try:
            a = MyInt(arg)
        except ValueError as ex:
            print(ex)
            a = None
        finally:
            print('  a : {} = {}'.format(type(a), a))
LightCC
  • 9,804
  • 5
  • 52
  • 92
0

Use GetAttr to Make a Normal Class Object return a Wrapped "Private" Attribute

Here's another answer I ran across that is worthy of a entry here. As it is buried at the end of an answer that is not the accepted answer to a somewhat different question, I am reposting it here.

I've been searching for this specific alternative for a number of cases, and here it actually is!! It seems when querying any object directly, there should be some way of replying however you want when you design a class. That is what this answer does.

So, for example, you can use a normal class, but have a query of a class object return an int. This allows the object to store the int in a separate variable so it is mutable, but make it easy to use the class object by not requiring access of a particular field, just querying the object directly (i.e. myint = MyInt(1); print(myint)

Note his recommendation first to really consider why you can't just subclass the appropriate type first (i.e. use one of the other answers here).


The Alternate Answer

This is copied from this StackOverflow answer. All credit to Francis Avila. This is using list instead of int as the subclass, but same idea:


  1. Take a look at this guide to magic method use in Python.
  2. Justify why you are not subclassing list if what you want is very list-like. If subclassing is not appropriate, you can delegate to a wrapped list instance instead:
class MyClass(object):
    def __init__(self):
        self._list = []
    def __getattr__(self, name):
        return getattr(self._list, name)

    # __repr__ and __str__ methods are automatically created
    # for every class, so if we want to delegate these we must
    # do so explicitly
    def __repr__(self):
        return "MyClass(%s)" % repr(self._list)
    def __str__(self):
        return "MyClass(%s)" % str(self._list)

This will now act like a list without being a list (i.e., without subclassing `list`).
```sh
>>> c = MyClass()
>>> c.append(1)
>>> c
MyClass([1])

LightCC
  • 9,804
  • 5
  • 52
  • 92