Python Data Types: The Complete Map

Python ships with a small set of built in types. Sorting them by three questions - single value or container, ordered or not, changeable or not - makes the whole set easy to hold in your head.

Why a map helps

Python has perhaps a dozen built in types that matter day to day. Learning them one by one produces a list to memorise. Learning them by the three questions that actually distinguish them produces understanding, and the list then remembers itself.

  1. Does it hold one value or many? Scalar or container.
  2. If many, is the order meaningful? Sequence or unordered collection.
  3. Can it be changed after creation? Mutable or immutable.

The map

TypeHoldsOrderedMutableWritten as
inta whole number-No42
floata decimal number-No3.5
complexa complex number-No2 + 3j
booltrue or false-NoTrue
strtextYesNo"hello"
listany valuesYesYes[1, 2, 3]
tupleany valuesYesNo(1, 2, 3)
setunique valuesNoYes{1, 2, 3}
frozensetunique valuesNoNofrozenset({1, 2})
dictkey to value pairsYes, by insertionYes{"a": 1}
bytesraw bytesYesNob"data"
NoneTypethe absence of a value--None
Since Python 3.7 a dictionary preserves insertion order as a guaranteed part of the language. Sets do not, and never will: a set has no notion of position at all.

Seeing the type of anything

print(type(42))          # <class 'int'>
print(type(3.5))         # <class 'float'>
print(type("hello"))     # <class 'str'>
print(type([1, 2]))      # <class 'list'>
print(type((1, 2)))      # <class 'tuple'>
print(type({1, 2}))      # <class 'set'>
print(type({"a": 1}))    # <class 'dict'>
print(type(True))        # <class 'bool'>
print(type(None))        # <class 'NoneType'>

The scalar types in one pass

int

Whole numbers of unlimited size. There is no 32 bit or 64 bit ceiling and no overflow; Python grows the number as far as memory allows.

print(2 ** 200)      # a 61 digit number, computed exactly
print(1_000_000)     # underscores are readability only, the value is 1000000

float

Decimal numbers stored in binary, which means they are approximations.

print(0.1 + 0.2)             # 0.30000000000000004
print(0.1 + 0.2 == 0.3)      # False

This is not a Python defect. Binary fractions cannot represent 0.1 exactly, in the same way decimal cannot represent one third exactly. For money and anywhere else exactness matters, use decimal.Decimal, covered in the standard library notes.

complex

z = 2 + 3j
print(z.real, z.imag)    # 2.0 3.0
print(abs(z))            # 3.605551275463989

Rarely needed outside engineering and mathematics, but it is a first class built in type, not an add on.

bool

print(True + True)               # 2
print(isinstance(True, int))     # True

bool is a subclass of int, with True equal to 1 and False equal to 0. That is occasionally useful, for example sum(scores > 50 for scores in results) counts how many passed. It also explains some surprising output.

None

result = None
print(result is None)      # True, the correct test
# if result == None:       # works, but never written this way

None means "no value here". It is a single object: every None in a program is the same object, which is why is is the right comparison. A function with no return statement returns None.

The container types in one pass

names = ["Meera", "Arun", "Sara"]        # list: ordered, changeable, duplicates allowed
point = (12.5, 48.2)                     # tuple: ordered, fixed, a single record
seen = {"Meera", "Arun"}                 # set: unordered, unique, fast membership
ages = {"Meera": 27, "Arun": 31}         # dict: look a value up by a key
label = "Invoice 2024"                   # str: an ordered sequence of characters

Choosing between them

You need toUseBecause
Keep items in order and change themlistOrder is preserved and the list can grow.
Group a few fields that belong togethertupleFixed size, cannot be modified by mistake, usable as a dictionary key.
Remove duplicates, or test membership oftensetMembership is roughly constant time regardless of size.
Look something up by a name or iddictDirect key access instead of scanning.
Work with textstrImmutable, and every text method is already built in.

Mutable and immutable

This is the distinction that causes the most confusion later, so it deserves stating plainly here.

  • Immutable: int, float, complex, bool, str, tuple, frozenset, bytes, None.
  • Mutable: list, set, dict, bytearray, and almost every class you write yourself.
text = "hello"
# text[0] = "H"        # TypeError: str does not support item assignment
text = "Hello"         # this is a new string, bound to the same name

items = [1, 2, 3]
items[0] = 99          # the list itself changed
print(items)           # [99, 2, 3]

Only immutable objects can be dictionary keys or set members, because a key that could change would break the lookup. That is the practical reason the distinction exists.

Dynamic typing, one more time

value = 10
print(type(value))     # int
value = "ten"
print(type(value))     # str

The name changed what it points at. Neither object changed type. Types belong to objects, never to names.

Common mistakes

  • Comparing floats with ==. Compare with a tolerance, or use math.isclose.
  • Writing {} and expecting an empty set. That is an empty dictionary; an empty set is set().
  • Writing (5) and expecting a tuple. That is the number 5 in brackets; a one element tuple is (5,).
  • Using == None instead of is None.
  • Trying to use a list as a dictionary key, then being puzzled by TypeError: unhashable type.
  • Assuming a set keeps the order you inserted in. It does not.

Best practices

  • Pick the container from the operation you perform most, not from habit.
  • Prefer a tuple when the collection should not change; it documents intent and protects you.
  • Use isinstance(x, int) rather than type(x) == int, so subclasses are handled correctly.
  • Keep money out of float.

Practice

  1. For each of these, name the type you would choose and say why in one line: unique visitor ids; a row read from a CSV file; a configuration file loaded into memory; the coordinates of a point; a queue of pending jobs.
  2. Predict and then explain the output of print(True + True + False).
  3. Explain why {"a": 1} and {1, 2} use the same brackets without ambiguity, and what {} means.
  4. Show two different ways to test whether 0.1 + 0.2 is close enough to 0.3.
  5. Group all the built in types listed above into mutable and immutable from memory, then check yourself.

Conclusion

Three questions - one value or many, ordered or not, changeable or not - place every built in type on the map. Everything that follows in this path, from slicing to dictionary keys to copying, is decided by where a type sits on it.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.