Skip to main content
Python · lesson 4 of 34
Python beginner Lesson 4 of 34

Python Data Types

Master Python's built-in data types — int, float, str, bool, None — and how to inspect and convert between them with real type conversion.

By SkillByExample · Updated

The Core Built-in Types

Python has a small set of primitive types that everything else is built from. Getting them right early prevents bugs that are surprisingly hard to track down later — especially around float precision, boolean coercion, and the None vs falsy value distinction. Output below is from Python 3.13.

int

Python integers have arbitrary precision — there is no overflow, no matter how large the number gets. This is different from most other languages, where integers wrap around at a fixed bit size.

big = 10 ** 100          # a googol: no overflow
print(len(str(big)))     # 101 digits
print(2 ** 64 + 1)       # past the 64-bit limit other languages hit

print(0xFF, 0o17, 0b1010)   # hex, octal and binary literals
print(1_000_000)            # underscores are only for readability
print(type(big))
$ python ints.py
101
18446744073709551617
255 15 10
1000000
<class 'int'>

2 ** 64 + 1 would wrap around in a language with fixed 64-bit integers; Python just allocates more digits. The other bases are only different ways to write a literal: once parsed, 0xFF is the ordinary int 255, and the underscores in 1_000_000 vanish entirely.

Integer division behavior is one of Python 3’s key changes from Python 2 — / now always returns a float:

# types.py
print(7 // 2)    # floor division
print(7 / 2)     # true division
print(7 % 2)     # modulo
$ python types.py
3
3.5
1

// rounds toward negative infinity (floor division); / always returns a float (true division); % gives the remainder. In Python 2, / performed integer division when both operands were integers — a major source of bugs during the 2→3 transition.

float

Floats are 64-bit IEEE 754 doubles — the same format used by virtually every other language. The precision pitfall they carry is not a Python bug; it’s a consequence of representing decimal fractions in binary.

pi = 3.14159
sci = 1.5e10      # scientific notation
neg_exp = 2.5e-3  # negative exponent

# The classic precision pitfall — affects every language using IEEE 754
print(0.1 + 0.2 == 0.3)   # False!
print(0.1 + 0.2)           # the actual stored value

# Fix with round() when displaying to users
print(round(0.1 + 0.2, 1) == 0.3)   # True

# Fix with decimal for financial math — never use float for money
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))
$ python types.py
False
0.30000000000000004
True
0.3

Float arithmetic is exact for the IEEE 754 representation, but 0.1 has no exact binary representation — so 0.1 + 0.2 lands at 0.30000000000000004, not 0.3. round() fixes display; Decimal fixes computation. Never use float for money.

Special float values handle edge cases like division limits and invalid math:

import math

# These print without errors, but produce special sentinel values
print(math.inf)        # float infinity
print(-math.inf)       # negative infinity
print(math.inf - math.inf)   # nan: "not a number"
print(math.nan == math.nan)  # nan is not equal to anything, even itself
print(math.isnan(math.nan))

try:
    print(0.0 / 0.0)
except ZeroDivisionError as e:
    print(f"ZeroDivisionError: {e}")
$ python special_floats.py
inf
-inf
nan
False
True
ZeroDivisionError: float division by zero

Unlike C or JavaScript, Python does not return nan or inf for division by zero, even with floats: 0.0 / 0.0 raises ZeroDivisionError. nan comes from undefined operations like inf - inf, and because it compares unequal to itself, test for it with math.isnan(). math.inf is a handy starting value when searching for a minimum.

str

Strings are immutable sequences of Unicode code points. Every string method returns a new string — the original is never modified. Python 3 made all strings Unicode by default, which means you can use any language or emoji without special handling.

# strings.py
name = "Alice"
multiline = """Line one
Line two"""
raw = r"C:\Users\alice\file.txt"

age = 30
greeting = f"Hello, {name}! You are {age} years old."

result = f"2 + 2 = {2 + 2}"
formatted = f"Pi is approximately {3.14159:.2f}"

print(greeting)
print(result)
print(formatted)
print("hello".upper())
print("  spaces  ".strip())
print("a,b,c".split(","))
print("-".join(["a", "b"]))
print("hello world".replace("world", "Python"))
$ python strings.py
Hello, Alice! You are 30 years old.
2 + 2 = 4
Pi is approximately 3.14
HELLO
spaces
['a', 'b', 'c']
a-b
hello Python

Strings are sequences, so the same indexing and slicing syntax used on lists works here too:

# indexing.py
s = "Python"

print(s[0])
print(s[-1])
print(s[1:4])
print(s[::-1])
$ python indexing.py
P
n
yth
nohtyP

bool

bool is a subclass of int, which means True and False are literally 1 and 0. This has practical consequences — you can sum a list of booleans to count how many are True.

# bool_arithmetic.py
print(True + True)
print(bool(0))
print(bool(1))
print(bool(""))
print(bool("hello"))
print(bool([]))
print(bool([0]))
$ python bool_arithmetic.py
2
False
True
False
True
False
True

bool is a subclass of int, so True + True equals 2. Remember: bool([0]) is True because the list is non-empty — even though 0 inside it is falsy.

Memorizing Python’s falsy values prevents bugs in conditional checks. The bool() constructor reveals truthiness without needing an if statement:

# falsy_values.py
print(bool(False))    # False
print(bool(None))     # False
print(bool(0))        # False
print(bool(0.0))      # False
print(bool(""))       # False
print(bool([]))       # False
print(bool({}))       # False
$ python falsy_values.py
False
False
False
False
False
False
False

These are the common falsy values; the rest of the family is any other zero or empty container — 0j, Decimal(0), (), set(), b"", range(0) — and everything else is truthy. Remembering which values are falsy is the quickest way to avoid subtle bugs in conditional checks.

None

None is Python’s null value — it represents “no value” or “not yet set.” It’s a singleton: there is exactly one None object in any Python process. Use it to signal optional values, uninitialized state, or missing results. Always check for None with is, not ==, since is checks identity (same object in memory) — correct for a singleton.

# none_demo.py
result = None

if result is None:
    print("No result yet")

if result is not None:
    print("We have a result!")
else:
    print("Result is still None")

print(type(None))
$ python none_demo.py
No result yet
Result is still None
<class 'NoneType'>

type() and isinstance()

Inspecting types at runtime is useful for debugging and for writing functions that handle multiple input types. isinstance() is almost always preferable to type() because it respects inheritance.

# types.py
print(type(42))
print(type("hello"))
print(type(3.14))
print(type(True))
print(type(None))

# isinstance() is aware of inheritance — bool IS-A int
print(isinstance(42, int))
print(isinstance(True, int))
print(isinstance(42, (int, float)))
$ python types.py
<class 'int'>
<class 'str'>
<class 'float'>
<class 'bool'>
<class 'NoneType'>
True
True
True

Type Conversion

Python doesn’t implicitly convert between types (except in a few arithmetic cases). Explicit conversion makes your intent clear and avoids silent data loss.

# conversions.py
print(int("42"))        # parses a decimal string
print(int(3.9))         # truncates toward zero, does NOT round
print(float("3.14"))    # decimal string to float
print(str(100))         # number to string
print(bool(0))          # falsy value to False
print(list("abc"))      # string splits into individual characters

# int() accepts a base argument for non-decimal strings
print(int("FF", 16))    # hexadecimal
print(int("1010", 2))   # binary
$ python conversions.py
42
3
3.14
100
False
['a', 'b', 'c']
255
10

Type Hints

Type hints don’t affect runtime behavior — Python ignores them at execution time. Their value is for tooling: editors use them for autocomplete, mypy uses them to catch type errors before you run the code, and they serve as inline documentation for anyone reading the function signature.

from typing import Optional

def add(a: int, b: int) -> int:
    return a + b

def greet(name: str) -> str:
    return f"Hello, {name}"

def find_user(user_id: int) -> Optional[str]:
    if user_id == 1:
        return "Alice"
    return None  # explicit None is self-documenting

print(add(3, 5))
print(greet("Bob"))
print(find_user(1))
print(find_user(99))
$ python type_hints.py
8
Hello, Bob
Alice
None

In Python 3.10+, you can use the union syntax directly without importing from typing:

def find_user(user_id: int) -> str | None:
    if user_id == 1:
        return "Alice"
    return None

print(find_user(1))
$ python union_syntax.py
Alice

Common Pitfalls

Mutable default arguments — one of Python’s most infamous gotchas. Default argument values are evaluated once when the function is defined, not each time it’s called. A mutable default (like a list or dict) is shared across every call.

# mutable_default.py
def append_to_wrong(item, lst=[]):
    lst.append(item)
    return lst

result1 = append_to_wrong(1)
result2 = append_to_wrong(2)
print(result1)
print(result2)
$ python mutable_default.py
[1, 2]
[1, 2]

Even result1 prints [1, 2], although it was returned before 2 was appended. Both calls returned the same list object, the one default created when def ran, so result1 and result2 are two names for one list. The fix is to use None as the sentinel:

# correct_default.py
def append_to(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

result3 = append_to(1)
result4 = append_to(2)
print(result3)
print(result4)
$ python correct_default.py
[1]
[2]

Integer identity caching — CPython caches small integers (-5 to 256) as an optimization, which means is comparisons appear to work for small ints but break for larger ones. Never use is to compare integers.

# int_cache.py
a = int("256")
b = int("256")
print(a is b)   # True: within the cached range

a = int("257")
b = int("257")
print(a is b)   # False: outside the cache, two different objects
print(a == b)   # True: value equality still works

c, d = 257, 257
print(c is d)   # True, but only because the compiler reused one constant
$ python int_cache.py
True
False
True
True

The values are built with int("...") so each is created at runtime. Write 257 twice as a literal in the same script and the compiler may store a single constant, so is says True anyway, as the last line shows. That inconsistency is the point: whether is happens to work depends on caching and compiler details.

Always use == for value comparison, never is. The identity check is an implementation detail of CPython, not a language guarantee — other Python implementations (PyPy, Jython) may not cache at all.

Practice

1. Write a program that adds 0.1 + 0.2 and prints the result. Then use the decimal module to show the exact decimal addition. Predict the difference before you run it.

Solution
from decimal import Decimal

print(0.1 + 0.2)
print(Decimal("0.1") + Decimal("0.2"))
$ python float_precision.py
0.30000000000000004
0.3

The float gives 0.30000000000000004 — the nearest binary representation is slightly too large. Decimal("0.1") stores exactly one-tenth, so the sum is exactly 0.3.

2. Predict what bool() returns for 0, -1, " ", [] and [0], then run it.

Solution
print(bool(0))
print(bool(-1))
print(bool(" "))
print(bool([]))
print(bool([0]))
$ python bool_coercion.py
False
True
True
False
True

0 is falsy but any non-zero number — even -1 — is truthy. A non-empty string (even just a space) is truthy. An empty list is falsy, but [0] is truthy because the list has one element, even though that element (0) is itself falsy.

3. Write a function that returns True when given a bool but False when given a regular int. Use isinstance() to make it work.

Solution
def is_bool_not_int(value):
    return isinstance(value, bool)

print(is_bool_not_int(True))   # True  — a bool is a bool
print(is_bool_not_int(42))     # False — an int is not a bool
print(is_bool_not_int(1))      # False — even though 1 == True
$ python isinstance_bool.py
True
False
False

The tricky part: since bool subclasses int, isinstance(True, int) is also True. The check must distinguish a bool from a plain int — isinstance(value, bool) alone works since bool is more specific than int.

Frequently Asked Questions

Is Python dynamically typed?
Yes. Variables don't have fixed types — the type lives on the object, not the variable. You can add type hints for tooling without affecting runtime behavior.
What's the difference between == and is?
== checks value equality. is checks identity (same object in memory). Use == for value comparisons; reserve is for None checks.
Why does 0.1 + 0.2 != 0.3 in Python?
Floating-point numbers are stored in binary, and 0.1 cannot be represented exactly. Use the decimal module or round() when precision matters.
Can you use a list as a dictionary key?
No. Lists are mutable and therefore unhashable, so using one as a dictionary key raises TypeError. Convert it to a tuple, which is immutable and hashable, when you need a sequence as a key.
Why does a list default argument keep its old values between calls?
Default argument values are evaluated once, when the def statement runs, not on every call. A mutable default such as a list is therefore shared by every call that relies on it. Use None as the default and create the list inside the function body.