Python data types are the vocabulary every later lesson depends on, and the four you will use constantly are int, float, str and bool. The surprises are not in what they hold but in how they relate to each other: booleans are integers, integers are unbounded, floats are approximations, and every one of those facts has a line of code that will bite you if you have not seen it coming.
What are Python's core data types?
Python ships a small set of built-in types, and most of your code is built from them. These four cover the primitive values; the rest of the language adds containers for collections of them.
| Type | Example | Mutable | What it is for |
|---|---|---|---|
int | 42 | no | Whole numbers, with no size limit at all |
float | 3.14 | no | Decimal numbers stored as binary approximations |
str | "forty-two" | no | Text, stored as Unicode characters |
bool | True | no | Two values: truth and falsehood |
NoneType | None | n/a | The single "no value here" marker |
values = {
'count': 42,
'ratio': 3.14,
'label': 'forty-two',
'flag': True,
'missing': None,
'items': [1, 2, 3],
}
for name, value in values.items():
print(f'{name:<9} {type(value).__name__:<10} {value!r}')
Every value carries its own type at runtime. There is no declaration step, and the type of a variable can change between assignments, which is why you sometimes see a value behave like a number and then like text in the same function.
How do you check the type of a value?
Three tools, in increasing order of subtlety. type(value) returns the exact class. isinstance(value, some_type) asks whether the value is an instance of that class or any subclass of it. And value.__class__.__mro__ shows the whole inheritance chain, which is how you discover surprising relationships.
value = 42
print('type(value) ->', type(value))
print('isinstance(value, int) ->', isinstance(value, int))
print('mro of int ->', [c.__name__ for c in type(value).__mro__])
flag = True
print('mro of bool ->', [c.__name__ for c in type(flag).__mro__])
Those two inheritance chains are the whole story of the next section. bool inherits from int, which is why isinstance(True, int) is True even though type(True) is bool.
Prefer isinstance() over type(x) is int in application code, because it survives subclassing. Use the exact check only when the distinction genuinely matters, such as when a flag must not be accepted where a number is expected.
Why is bool a subclass of int?
Because Python treats booleans as the numbers zero and one. That is convenient in arithmetic and occasionally dangerous, since True quietly behaves as 1 in almost every numeric context.
print('True == 1 ->', True == 1)
print('True + True ->', True + True)
print('sum([True, False]) ->', sum([True, False]))
print('type(True) is int ->', type(True) is int)
print('isinstance(True, int) ->', isinstance(True, int))
print('isinstance(True, bool)->', isinstance(True, bool))
print('bool(1) and bool(0) ->', bool(1), bool(0))
print('bool("False") ->', bool('False'))
print('bool("") ->', bool(''))
print('bool([]) ->', bool([]))
print('bool([0]) ->', bool([0]))
Read that output carefully. bool('False') is True, because a non-empty string is truthy no matter what it says. Empty containers are falsy, but a container holding a zero is truthy, because the container itself is not empty. This is the source of the classic bug where a form field containing the text "False" is treated as a checked box.
The practical rule: use is True or is None when you need to test a value's identity rather than its truthiness, and never rely on a string's contents to decide something.
Where do numbers surprise you?
Two of them, and both are worth understanding rather than memorising.
The first is that integers have no upper limit. Python's integers grow as large as you need them to, so a calculation that would overflow a fixed-width integer in another language simply keeps working. The second is that floats are binary approximations, and decimal fractions such as 0.1 cannot be represented exactly in binary. The result is the famous line of output that trips up everyone once.
import math
import sys
from decimal import Decimal
huge = 10 ** 40
print('int with 40 digits ->', huge)
print('int never overflows ->', 10 ** 1000 > 0)
total = 0.1 + 0.2
print('0.1 + 0.2 ->', total)
print('equals 0.3? ->', total == 0.3)
print('isclose? ->', math.isclose(total, 0.3))
print('float precision ->', sys.float_info.mant_dig, 'binary digits')
print('float max ->', sys.float_info.max)
print('Decimal total ->', Decimal('0.10') + Decimal('0.20'))
print('float total ->', 0.10 + 0.20)
For approximate measurements, floats are exactly right and you should compare them with math.isclose() rather than ==. For money, use decimal.Decimal and build it from a string, as in Decimal('0.10'), because constructing it from a float inherits the very error you were trying to avoid.
How do you pick the right type?
intfor counts, indices, identifiers, and anything that must be exact. Integer arithmetic is exact and fast.floatfor physical measurements, ratios, and averages, where a tiny approximation is acceptable.Decimalfor money and anything a customer will be charged. It stores decimal digits exactly.strfor text, and for numbers that arrive from the outside world as text, which is most of the time.boolfor genuine yes/no values. Do not use0and1as booleans in code you intend to read later.
Two more habits keep types honest. Print type(value).__name__ when a value surprises you, and compare values with repr() when the difference is invisible in printed output, because repr shows quotes and escapes that reveal hidden characters.
It also helps to remember that str() and repr() are not the same function. str() asks an object for the version meant for people, and repr() asks for the version meant for developers, which is usually more precise. For a float they usually agree; for a string they do not, because repr() adds the quotes that let you see an accidental trailing space.
import sys
def describe(value):
kind = type(value).__name__
size = sys.getsizeof(value)
return f'{kind:<10} {value!r:<26} {size} bytes'
for candidate in (0, 42, 10 ** 30, 3.14, '', 'hello', True, None, [1, 2], (1, 2), {'a': 1}):
print(describe(candidate))
The byte counts are approximate and vary between builds and platforms, so treat them as a rough comparison rather than a specification. What the table reliably shows is that a small integer and a small string cost far less memory than a list holding the same number of items, which matters when you are processing large volumes of data.
Your turn: build a type detective
- Write a function
classify(value)that returns a short label for any value: "empty text", "whole number", "decimal number", "yes or no", "nothing", "list", or "something else". - Feed it a deliberate mix:
0,-7,3.0,3.5,'','0',False,None,[],[0],{},set(). Print each label next to the value and its type name. - Add a guard so that
Trueis reported as "yes or no" and never as "whole number", even though it passesisinstance(value, int). Order your checks with the most specific first. - Now handle the money case: write
add_prices(a, b)that takes two strings, converts them withDecimal, and returns the exact sum. Confirm thatadd_prices('0.10', '0.20')gives exactly0.30. - Finally, compare
0.1 + 0.2 == 0.3withmath.isclose(0.1 + 0.2, 0.3)in your script and print both. Understanding why one says False and the other says True is the whole point of this exercise.
The mental model to keep
- Every value knows its own type at runtime; you never declare one.
type()is exact,isinstance()understands inheritance, and for booleans those two answers differ.- Booleans are integers, so
Trueequals 1 and a non-empty string like "False" is truthy. - Integers are exact and unbounded; floats are binary approximations, so compare them with
math.isclose(). - Money belongs to
Decimal, constructed from a string.