Memory-compact design
Your task is clearly directed towards a memory light-weight footprint of some 2B-values.
The solution in Python has a few more things to take into account.
Assuming the range of values, meeting the range-of-values represented within a 2B-scope of int-s ( a range of numbers 0..65535 ) to be memory-efficient is just an illusion.
Your design shall take into account, as a minimum, the overheads of associated with an object's "internal_representation" + the overheads associated with object-instance's access-methods, to get a number "there" and "back" ( not speaking about performance impact, as your requirement was focused on low-memory-footprint, but the smarter are the proxy-data-structures, the bigger typically grow the data-structure access/maintenance CPU-costs ).
If your main motivation was to design for a low-memory foot-print, the worst-case scenario shall sum up all these static memory-allocations + the requirements for any dynamic-allocations, requested during the access-methods' and object-modifications' operations + the size of the import-ed module(s) sizes.
Once speaking about numpy -- a great tool -- you would get a great & powerful library for fast, vectorised array manipulations, with a given cell-data representation ( dtype = numpy.uint8 ).
The hidden-part ( for any assumption on the code-design low-memory-footprint ) is to take into account the overall memory-cost of the approach.
Compare not just an object size but also the Class-related overhead ( immense list of numpy methods not shown for clarity ):
>>> anIntOBJECT= 8
>>> anIntOBJECT.__sizeof__() # ____________________________instance-method works
12
>>> sys.getsizeof(anIntOBJECT) # kindly do not modify the MCVE with erroneous code
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined
>>> dir(anIntOBJECT)
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__format__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__index__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'imag', 'numerator', 'real']
>>> len( dir( anIntOBJECT ) ) # Class-methods
64
Another approach with a string-based data-representation:
>>> aStrOBJECT = "08"
>>> aStrOBJECT.__sizeof__() # ____________________________instance-method works
23
>>> sys.getsizeof(aStrOBJECT) # kindly do not modify the MCVE with erroneous code
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined
>>> dir( aStrOBJECT )
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> len( dir( aStrOBJECT ) )
71
One more primary archetype for a mutable-sequence:
>>> aBytearrayOfINTs = bytearray() # in python base
>>> aBytearrayOfINTs.__sizeof__() # _________________________instance-method works
24
>>> dir( aBytearrayOfINTs )
['__add__', '__alloc__', '__class__', '__contains__', '__delattr__', '__delitem__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'capitalize', 'center', 'count', 'decode', 'endswith', 'expandtabs', 'extend', 'find', 'fromhex', 'index', 'insert', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'pop', 'remove', 'replace', 'reverse', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
>>> len( dir( aBytearrayOfINTs ) )
76
Real data-structure implementation: access & maintenance methods decide
In all cases, the data-structure access/maintenance methods will be a deciding factor, maybe more, than just the single-value ( minimum representation ) memory-footprint expectation.
numpyarrays, you can type them, e.g.np.array([100, 200, 300], dtype=np.uint8)- seenumpy.dtype.