'StrictInt' Python object class that prohibits casting numbers with non-integer components to `int` Inspired by and created because of python3 utility: convert safely to int and driven partly by the 'refactor' in my answer.


The aforementioned question and my answer drove me to go and do a thing while bored at work - I created a new object class called StrictInt that behaves much like the function created by the original poster of that question/thread, but as an object class instead.

This object class is a subclass of int (which is  the superclass.  It's designed to work just like an int, but when you cast a str, int, or float to the StrictInt object, it will only convert the value if it is indeed an integer.

Because I'm crazy, you'll also need the typing module, but most Python 3 installs have that, if not, grab it from pip3.

This is the code used for the StrictInt class, and any required imports, in the strictint.py file:

from typing import Union, Any


class StrictInt(int):
    def __new__(cls, value, *args, **kwargs):
        # type: (Union[int, float, str], Any, Any) -&gt; int

        if not isinstance(value, (int, StrictInt, float, str)):
            t = str(type(value)).replace("&lt;class '", "").replace("'&gt;", "")
            raise TypeError("Cannot convert type '{type}' to strict integer".format(type=t))

        try:
            f = float(value)
        except ValueError:
            f = None

        if not f:
            raise ValueError("Cannot convert a non-number to a strict integer.")

        if not f.is_integer():
            raise ValueError("Cannot convert value due to non-integer parts.")

        return super(StrictInt, cls).__new__(cls, int(f))


There is a set of unit tests I've been using as well (tests.py):

from strictint import StrictInt
import unittest


class TestStrictInt(unittest.TestCase):

    def test_float_conversion(self):
        # Non-integer parts present in a float, should raise ValueError
        self.assertRaises(ValueError, StrictInt, 3.14159)
        # Float that is equal to an int should be equal.
        self.assertEqual(3.0, StrictInt(3.0))

    def test_ints(self):
        # int(3) should equal StrictInt(3).
        self.assertEqual(3, StrictInt(3))

    def test_nonnumeric_string(self):
        # Not a number at all.
        self.assertRaises(ValueError, StrictInt, "I Am A Teapot")
        # Number with an invalid character in it, so Not a Number.
        self.assertRaises(ValueError, StrictInt, " 3.14159")
        # Has numeric content, but not a valid number due to dots.
        self.assertRaises(ValueError, StrictInt, "3.14.156")

    def test_numeric_string(self):
        # int('3') should equal StrictInt('3')
        self.assertEqual(int('3'), StrictInt('3'))
        # int(float('3.0')) is equal to int(3.0), and should equal StrictInt('3.0')
        self.assertEqual(int(float('3.0')), StrictInt('3.0'))
        # String with a number that has a decimal part should raise ValueError
        self.assertRaises(ValueError, StrictInt, '3.14159')


if __name__ == '__main__':
    unittest.main(warnings='ignore')


Any and all improvement suggestions are welcome.  This works pretty well and fairly quickly from what I've tested, but I value everyone's opinions.
Converting everything via a float means that you get the wrong result whenever the input cannot be represented exactly as a double-precision floating-point number. For example, this is surely not acceptable:

&gt;&gt;&gt; StrictInt(10**23)
99999999999999991611392

There's an OverflowError when the input is too large to be represented as a float:

&gt;&gt;&gt; StrictInt(10**400)
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "cr195375.py", line 13, in __new__
    f = float(value)
OverflowError: int too large to convert to float

The strategy of enumerating the allowable types (int, StrictInt, float, str) means that many plausible use cases are disallowed, for example with fractions.Fraction:

&gt;&gt;&gt; from fractions import Fraction
&gt;&gt;&gt; StrictInt(Fraction(10, 1))
Traceback (most recent call last):
  File "&lt;stdin&gt;", line 1, in &lt;module&gt;
  File "cr195375.py", line 10, in __new__
    raise TypeError("Cannot convert type '{type}' to strict integer".format(type=t))
TypeError: Cannot convert type 'fractions.Fraction' to strict integer


and similarly with decimal.Decimal.
The Pythonic approach is to use duck typing — that is, instead of testing whether input belongs to a fixed collection of types, you call the appropriate methods on the input. Here we want to determine whether the input has a non-zero fractional part, and otherwise to use the integer part. Hence we need Python's built-in divmod function:

quotient, remainder = divmod(value, 1)
if remainder:
    raise ValueError("could not convert value due to non-zero "
                     f"fractional part: {val!r}")

There is no point taking *args and **kwargs if you are not going to use them.


Putting this together:

class StrictInt(int):
    "Subclass of int that refuses to coerce non-integer values."
    def __new__(cls, value):
        if isinstance(value, str):
            for converter in (int, float, complex):
                try:
                    value = converter(value)
                    break
                except ValueError:
                    pass
            else:
                raise ValueError(f"invalid literal for {cls.__name__}(): "
                                 f"{value!r}")
        if value.imag:
            raise ValueError("could not convert value due to non-zero "
                             f"imaginary part: {value!r}")
        quotient, remainder = divmod(value.real, 1)
        if remainder:
            raise ValueError("could not convert value due to non-zero "
                             f"fractional part: {value!r}")
        return super(StrictInt, cls).__new__(cls, int(quotient))


This handles a wider ranges of input types:

&gt;&gt;&gt; StrictInt(Fraction(10, 2))
5
&gt;&gt;&gt; StrictInt(Decimal('11.00000'))
11
&gt;&gt;&gt; StrictInt(5+0j)
5


and it copes with integers that are too large to be represented as floats:

&gt;&gt;&gt; StrictInt(10**400) == 10**400
True