Что такое типы данных и в каких случаях лучше использовать определённый тип данных?
В Python все типы данных делятся на изменяемые (англ. mutable) и неизменяемые (англ. immutable). Неизменяемые типы данных (например, строка, кортеж, frozenset) имеют несколько особенностей:
- занимают меньше места в памяти, чем их изменяемые аналоги;
- являются хэшируемыми (англ. hashable).
Хэшируемые типы данных в отличие от нехэшируемых можно использовать в качестве ключей словарей, а также они могут быть элементами множеств.
>>> a_set = frozenset({1, 2, 3})
>>> hash(a_set)
-272375401224217160
>>> a_list = [1, 2, 3]
>>> hash(a_list)  # Ошибка: список - нехэшируемый тип данных
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> a_dict = {[1, 2, 3]: 'example_value'}  # Ошибка: нельзя использовать нехэшируемые типы данных как ключи словаря
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> a_dict = {(1, 2, 3): 'example_value'}  # но можно использовать хэшируемые
>>> a_dict
{(1, 2, 3): 'example_value'}
Неизменяемые типы данных имеют меньший размер (объём занимаемой оперативной памяти):
>>> import sys
>>> sys.getsizeof([1, 2, 3])  # Изменяемый тип данных list
88
>>> sys.getsizeof((1, 2, 3))  # Неизменяемый тип данных tuple
64

Вывод: используйте неизменяемые аналоги изменяемых типов данных тогда, когда вы хотите оптимизировать быстродействие своей программы и точно знаете, что вам никогда не потребуется изменять значения внутри структуры данных (например, когда вы получаете данные из внешнего источника по протоколу HTTP и читаете их), либо когда данные нужно использовать в качестве ключа словаря или использовать внутри множества.
Любой тип это класс, через классы можно реализовать любой тип какой пожелаешь.

вот например в базе postgresql есть тип smalllint размером в 2 байта и туда записать можно значение в диапазоне от -32768 до 32767, чтобы контролировать это на уровне питона можно свой тип сделать
class int2(int):

    def __init__(self, x) -> None:
        if not -32768 <= x <= 32767: raise TypeError
        super().__init__()