I want a python type-hint friendly way to create a Type that has constrained range of values.
For example, a URL Type based on type str that would only accept strings that look like an "http" URL.
# this code is made up and will not compile
class URL(typing.NewType('_URL', str)):
    def __init__(self, value: str, *args, **kwargs):
        if not (value.startswith('http://') or value.startswith('https://')):
            raise ValueError('string is not an acceptable URL')
                str; http URL stringsHere is an example overriding str.  This does not require the typing module but still works with type-hinting.
This str derived class asserts the initialized string looks like an http URL string.
class URL(str):
    def __new__(cls, *value):
        if value:
            v0 = value[0]
            if not type(v0) is str:
                raise TypeError('Unexpected type for URL: "%s"' % type(v0))
            if not (v0.startswith('http://') or v0.startswith('https://')):
                raise ValueError('Passed string value "%s" is not an'
                                 ' "http*://" URL' % (v0,))
        # else allow None to be passed. This allows an "empty" URL instance, e.g. `URL()`
        # `URL()` evaluates False
        return str.__new__(cls, *value)
This results in a class that will only allow some strings.  Otherwise, it behaves like an immutable str instance.
# these are okay
URL()
URL('http://example.com')
URL('https://example.com')
URL('https://')
# these raise ValueError
URL('example')  # ValueError: Passed string value "example" is not an "http*://" URL
URL('')  # ValueError: Passed string value "" is not an "http*://" URL
# these evaluate as you would expect
for url in (URL(),  # 'False'
            URL('https://'),  # 'True'
            URL('https://example.com'),  # 'True'
           ):
    print('True') if url else print('False')
(update: later on I found the purl Python library)
Another example,
int; constrained integer range Number
This int derived class only allows values 1 through 9 inclusive.
This has a special feature, too.  In case an instance is initialized with nothing (Number()) then that value equates to 0 (this behavior is derived from the int class). In that case, the __str__ should be a '.' (program requirement).
class Number(int):
    """integer type with constraints; part of a Sudoku game"""
    MIN = 1  # minimum
    MAX = 9  # maximum
    def __new__(cls, *value):
        if value:
            v0 = int(value[0])
            if not (cls.MIN <= v0 <= cls.MAX):
                raise ValueError('Bad value "%s" is not acceptable in'
                                 ' Sudoku' % (v0,))
        # else:
        #    allow None to be passed. This allows an "empty" Number instance that
        #    evaluates False, e.g. `Number()`
        return int.__new__(cls, *value)
    def __str__(self):
        """print the Number accounting for an "empty" value"""
        if self == 0:
            return '.'
        return int.__str__(self)
This ensures errant inputs are handled sooner rather than later.  Otherwise, it behaves just like an int.
# these are okay
Number(1)
Number(9)
Number('9')
# this will evaluate True, just like an int
Number(9) == int(9)
Number('9') == int(9)
Number('9') == float(9)
# this is okay, it will evaluate False
Number()
print('True') if Number() else print('False')  # 'False'
# these raise ValueError
Number(0)  # ValueError: Bad value "0" is not acceptable in Sudoku
Number(11)  # ValueError: Bad value "11" is not acceptable in Sudoku
Number('11')  # ValueError: Bad value "11" is not acceptable in Sudoku
And the special "feature"
print(Number(1)) # '1' (expected)
print(Number())  # '.' (special feature)
Technique for inheriting immutable types is derived from this SO answer.
Subclassing builtin types can lead to some odd cases (consider code which checks exactly type(...) is str)
Here is a pure-typing approach which is typesafe and fully preserves the type of your strings:
from typing import NewType
_Url = NewType('_Url', str)
def URL(s: str) -> _Url:
    if not s.startswith('https://'):
        raise AssertionError(s)
    return _Url(s)
print(type(URL('https://example.com')) is str)  # prints `True`
The approach here "hides" the runtime checking behind a function which looks like a constructor from an api perspective, but in reality is just a tiny type (I couldn't find a canonical reference to "tiny types" this appears to just be the best resource I could find).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With