Python: Data Types (int, float, str, bool, complex, bytes, bytearray, None)

Ask Python what 28 is and it answers int. Wrap the same characters in quotes and they become str, and suddenly + means glue instead of add. Python data types decide what every value can and cannot do. This guide covers all 8 built-in types, from int and float to bytes and None, with tested examples and the traps between them.

“Programs must be written for people to read, and only incidentally for machines to execute.”

Harold Abelson, SICP

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 19 minutes

Think about your kitchen for a second. Sugar goes in one jar, salt in another, oil in a bottle, and leftovers in a sealed box. Same kitchen, but each container is built for a specific kind of thing, and you would never store oil in the sugar jar. Python data types work the same way. Every value you create gets the right kind of container: a whole number, a decimal, some text, a yes/no flag. Get the container right and everything downstream just fits.

Here is the technical version of that idea. In Python, everything is an object, and every object has a type. When you write age = 28, Python does not just stash the number 28 somewhere. It builds an int object that holds the value 28, then attaches the name age to it. Learn the built-in types and you have learned the building blocks of every Python program you will ever write.

This post walks through all 8 of these built-in types. Not just what they are, but when you would actually reach for each one. If you finished the Variables post, you already saw type() in action. Now we will explore every type it can hand back to you.

The Type Hierarchy

Python organizes its built-in types into categories. Here’s the full picture before we explore each one.

Then Classified by MutabilityMutablelist, dict, set, bytearrayImmutableint, float, str, tuple, bool,bytes, frozenset, complex, range, NoneMapping, Set, Specialdict{‘k’: ‘v’}set{1, 2, 3}frozensetimmutable setNoneTypeNoneSequencelist[1, 2, 3]tuple(1, 2, 3)rangerange(10)Text and Binarystr‘hi’, \world\bytesb’hi’bytearraymutable bytesNumericint42, -7, 0float3.14, -0.5complex3+4jboolTrue, FalsePython Built-in TypesPython Data Types: Built-in Type Hierarchy by Category, Mutable vs Immutable

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

The diagram above shows every built-in type grouped by category. Look at the bottom row. Some types are mutable, which means you can change them after you create them. Others are immutable, so once they exist they never change. This one distinction matters more than you would guess, and you will see exactly why in the examples below.

Quick Reference Table

TypeExampleMutable?When to Use
int42, -7, 0NoCounting, indexing, IDs
float3.14, -0.5NoDecimals, measurements, money (careful!)
str‘hello’, “world”NoText, names, messages, file paths
boolTrue, FalseNoFlags, conditions, toggles
NoneNoneNo“No value”, default returns, placeholders
complex3+4jNoEngineering, signal processing (rare)
bytesb’hello’NoBinary data, network, file encoding
bytearraybytearray(b’hi’)YesMutable binary data, buffer manipulation

Integers (int)

Python integers have unlimited precision. Think of writing a number on paper: you never hit a “maximum”, you just keep adding digits. Unlike C or Java where integers overflow at some limit, Python handles numbers as large as your memory allows. In the example below we store the age of a developer named Rahul, then jump straight to a number with 101 digits.

📄 integers.py: Python integers have no size limit

# Basic integers
rahul_age = 28
temperature = -5
zero = 0

# Python handles arbitrarily large numbers
big_number = 10 ** 100    # a googol
print(f"Rahul's age: {rahul_age}")
print(f"A googol has {len(str(big_number))} digits")
print(f"Type: {type(rahul_age)}")

# Different bases
binary = 0b1010          # binary = 10
octal = 0o17             # octal = 15
hexadecimal = 0xFF       # hex = 255
print(f"Binary 1010 = {binary}, Octal 17 = {octal}, Hex FF = {hexadecimal}")

# Underscores for readability (Python 3.6+)
population = 1_400_000_000
print(f"Population: {population:,}")

▶ Output

Rahul's age: 28
A googol has 101 digits
Type: <class 'int'>
Binary 1010 = 10, Octal 17 = 15, Hex FF = 255
Population: 1,400,000,000

What happened here: Python made an int object for each value. The 10 ** 100 calculation produced a 101-digit number without breaking a sweat. Try that in Java and you hit an overflow. The underscores in 1_400_000_000 are purely for your eyes, like the commas you write in a big number on paper. Python ignores them completely. And the 0b, 0o, and 0x prefixes let you write a number in binary, octal, or hex whenever that feels more natural than plain decimal.

Floating Point (float)

Floats are how Python stores decimal numbers. A float is like a measuring tape: great for height, weight, and temperature, but it can only measure so finely, and past that it quietly rounds. Under the hood floats use 64-bit IEEE 754 double precision, which sounds rock solid until you watch what happens with 0.1 + 0.2.

📄 floats.py: floats and their famous precision trap

height = 5.9
pi = 3.14159265358979

print(f"Height: {height}")
print(f"Pi: {pi}")
print(f"Type: {type(height)}")

# The famous floating point surprise
result = 0.1 + 0.2
print(f"0.1 + 0.2 = {result}")      # not 0.3!
print(f"0.1 + 0.2 == 0.3? {result == 0.3}")

# Scientific notation
speed_of_light = 3e8     # 300,000,000
tiny = 1.5e-10           # 0.00000000015
print(f"Speed of light: {speed_of_light}")
print(f"Tiny: {tiny}")

# Special float values
print(f"Infinity: {float('inf')}")
print(f"Not a Number: {float('nan')}")

▶ Output

Height: 5.9
Pi: 3.14159265358979
Type: <class 'float'>
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3? False
Speed of light: 300000000.0
Tiny: 1.5e-10
Infinity: inf
Not a Number: nan

What happened here: 0.1 + 0.2 gives 0.30000000000000004, not the clean 0.3 you expected. This is not a Python bug. It is how every computer stores decimals in binary. The value 0.1 cannot be written exactly in binary, in the same way 1/3 cannot be written exactly in decimal (0.333… goes on forever). So the tiny error you see is just that rounding leaking through. For money, reach for the decimal module instead of floats, which we cover in the Standard Library post later.

Strings (str)

A string is a sequence of Unicode characters, which is the technical way of saying “text.” Picture beads on a thread: each character sits at a fixed position, you can look at any bead you like, but you cannot swap one out without stringing a whole new thread. Python 3 handles Unicode natively, so for most everyday work you get no encoding headaches at all. The examples below store the names of three users, Niranjan, Viraj, and Prathamesh, exactly the kind of text every real app deals with.

📄 strings.py: strings are immutable sequences of characters

# Three ways to create strings
single = 'Niranjan'
double = "Viraj Patil"
multi = """This string
spans multiple
lines"""

print(single, "|", double)
print(multi)
print(f"Type: {type(single)}")

# Strings are sequences, so indexing and slicing work
name = "Prathamesh"
print(f"First char: {name[0]}")
print(f"Last char: {name[-1]}")
print(f"First 4: {name[:4]}")
print(f"Length: {len(name)}")

# Strings are IMMUTABLE, so you cannot change them in place
# name[0] = "p"   # TypeError: 'str' object does not support item assignment
new_name = "p" + name[1:]   # create a NEW string instead
print(f"Modified: {new_name}")

▶ Output

Niranjan | Viraj Patil
This string
spans multiple
lines
Type: <class 'str'>
First char: P
Last char: h
First 4: Prat
Length: 10
Modified: prathamesh

What happened here: Strings behave like sequences. You can index them with [0], slice them with [:4], and measure them with len(). But they are immutable, so you cannot change one character in place. To “edit” a string, you build a brand new one, which is exactly what "p" + name[1:] does above. That is also why string methods like .upper() and .replace() always hand you a new string instead of touching the original. We dig into every string method in the string methods tutorial.

Booleans (bool)

A boolean is a simple light switch: it is either True (on) or False (off), nothing in between. Here is the part most beginners never hear: bool is actually a subclass of int. True is literally 1 and False is literally 0. The switch is just a number wearing a friendlier name. In the demo below we also test all kinds of values for truthiness, including the name of a user called Anvi.

📄 booleans.py: booleans are integers in disguise

is_active = True
is_admin = False

print(f"is_active: {is_active}, type: {type(is_active)}")

# bool is a subclass of int, and yes, this is real
print(f"True + True = {True + True}")
print(f"True * 10 = {True * 10}")
print(f"False + 42 = {False + 42}")
print(f"isinstance(True, int) = {isinstance(True, int)}")

# Truthy and Falsy values
print(f"bool(0): {bool(0)}")
print(f"bool(1): {bool(1)}")
print(f"bool(''): {bool('')}")
print(f"bool('Anvi'): {bool('Anvi')}")
print(f"bool([]): {bool([])}")
print(f"bool([1]): {bool([1])}")
print(f"bool(None): {bool(None)}")

▶ Output

is_active: True, type: <class 'bool'>
True + True = 2
True * 10 = 10
False + 42 = 42
isinstance(True, int) = True
bool(0): False
bool(1): True
bool(''): False
bool('Anvi'): True
bool([]): False
bool([1]): True
bool(None): False

What happened here: Since bool inherits from int, you can do math with booleans. True + True is 2 because True equals 1. More practically, every value in Python has a “truthiness.” Empty containers ([], {}, ''), zero (0, 0.0), and None are all falsy. Everything else is truthy. This is why you can write if my_list: instead of if len(my_list) > 0:.

None, the Absence of Value

None is Python’s way of saying “nothing here.” Picture an empty parking spot. The spot exists, but no car is in it. That is None. It is not zero, not an empty string, not False. It is the deliberate, explicit absence of any value. In the example below, a function greets a user named Vinay, and we catch what it quietly hands back.

📄 none_type.py: None is not zero, not empty, not False

# None is its own type
result = None
print(f"result: {result}")
print(f"type: {type(result)}")

# Functions without explicit return give None
def greet(name):
    print(f"Hello, {name}!")
    # no return statement

value = greet("Vinay")
print(f"Return value: {value}")

# Always use 'is' to check for None, never ==
x = None
print(f"x is None: {x is None}")      # correct way
print(f"x == None: {x == None}")        # works but discouraged

# None is falsy
if not result:
    print("result is falsy (None)")

▶ Output

result: None
type: <class 'NoneType'>
Hello, Vinay!
Return value: None
x is None: True
x == None: True
result is falsy (None)

What happened here: When greet() has no return statement, Python quietly returns None for you. This trips up a lot of beginners, because they assume the function returns whatever it printed. Printing and returning are two completely different things. Always test for None with is, not ==. The reason: is checks identity, and there is only ever one None object in the whole program, while == can be overridden by a custom class and give you a surprising answer.

Complex Numbers

Python has built-in support for complex numbers. Think of the spare wheel in your car: most days you forget it exists, but it ships with the car anyway. Honestly, you will probably never touch these unless you work in signal processing, electrical engineering, or scientific computing. But they are baked right into the language, ready the moment you need them.

📄 complex_numbers.py: built-in complex number support

# j is the imaginary unit (not i; Python uses j like electrical engineering)
z = 3 + 4j
print(f"z = {z}")
print(f"Real part: {z.real}")
print(f"Imaginary part: {z.imag}")
print(f"Type: {type(z)}")

# Complex arithmetic
z2 = 1 - 2j
print(f"z + z2 = {z + z2}")
print(f"|z| (magnitude) = {abs(z)}")    # sqrt(3² + 4²) = 5.0

▶ Output

z = (3+4j)
Real part: 3.0
Imaginary part: 4.0
Type: <class 'complex'>
z + z2 = (4+2j)
|z| (magnitude) = 5.0

What happened here: Python writes the imaginary unit as j (the electrical engineering convention), not i (the math convention). The .real and .imag attributes pull out the two halves of the number, and abs() gives you its magnitude. Most developers will never need complex numbers, but on the rare day you do, having them built in with no extra library to install is a genuine relief.

Bytes and Bytearray

These two types handle raw binary data, the actual bytes a computer stores underneath all your friendly text and numbers. If str is the printed letter you read, bytes is the raw ink pattern on the page. You will bump into them when you work with files in binary mode, network sockets, or an API (Application Programming Interface) that sends back binary instead of text. Below we take the name of a user, Pravin, and turn it into bytes and back.

📄 bytes_demo.py: binary data types

# bytes: immutable binary data
data = b"Hello"
print(f"data: {data}")
print(f"type: {type(data)}")
print(f"first byte: {data[0]}")    # 72 is the ASCII code for 'H'

# Converting between str and bytes
text = "Pravin"
encoded = text.encode("utf-8")    # str -> bytes
decoded = encoded.decode("utf-8") # bytes -> str
print(f"Encoded: {encoded}")
print(f"Decoded: {decoded}")

# bytearray: mutable version of bytes
buffer = bytearray(b"Hello")
buffer[0] = 104                   # lowercase 'h'
print(f"Modified buffer: {buffer}")

▶ Output

data: b'Hello'
type: <class 'bytes'>
first byte: 72
Encoded: b'Pravin'
Decoded: Pravin
Modified buffer: bytearray(b'hello')

What happened here: bytes stores raw binary data. Index into it and you get back an integer, the byte value, not a character. The b"..." prefix is how you write a bytes literal. bytearray is the mutable cousin: you can change individual bytes in place, which is handy when you build binary protocols or poke at a file buffer. In everyday Python, you will meet bytes mostly when you read a file in binary mode or handle data coming off the network.

Mutable vs Immutable

This is the single distinction that prevents most beginner Python bugs. Some types can be changed in place (mutable), others cannot (immutable). Think of a whiteboard versus a printed photo. You can wipe and rewrite the whiteboard as many times as you like (mutable), but to “change” a printed photo you have to print a brand new one (immutable). In the demo below, the name of a user, Aditi, plays the printed photo, and a list of scores plays the whiteboard.

📄 mutability.py: the difference that matters most

# IMMUTABLE int: reassignment creates a new object
score = 95
print(f"Before: id={id(score)}")
score = 96
print(f"After:  id={id(score)}")   # different id means a new object!

# IMMUTABLE str: methods return NEW strings
name = "Aditi"
upper_name = name.upper()
print(f"Original: {name}")          # unchanged
print(f"Upper: {upper_name}")       # new string

# MUTABLE list: changed IN PLACE, same object
scores = [88, 92, 76]
print(f"Before: {scores}, id={id(scores)}")
scores.append(95)
print(f"After:  {scores}, id={id(scores)}")  # same id!

▶ Output

Before: id=140234863257200
After:  id=140234863257232
Original: Aditi
Upper: ADITI
Before: [88, 92, 76], id=140234866019072
After:  [88, 92, 76, 95], id=140234866019072

What happened here: When we changed score from 95 to 96, the id() changed too, because Python built a completely new int object. Integers are immutable, so you can never modify the value 95 itself. But when we appended to scores, the id() stayed exactly the same, because the list was edited in place. This is the very thing we covered in the variables tutorial with the b = a reference trap. Once mutability clicks, that trap stops being a mystery.

Common Mistakes

Mistake 1: Using float for money

🚫 Wrong

price = 19.99
tax = price * 0.1
total = price + tax
print(f"Total: {total}")   # 21.988999999999997, not a clean 21.989!

✅ Correct

from decimal import Decimal
price = Decimal("19.99")
tax = price * Decimal("0.1")
total = price + tax
print(f"Total: {total}")   # 21.989, exact

Why: Floats can’t represent 0.1 exactly in binary. For financial calculations, use decimal.Decimal with string inputs (not float inputs) for exact arithmetic.

Mistake 2: Checking type with == instead of isinstance()

🚫 Fragile

value = True
if type(value) == int:
    print("It's an int")    # nothing prints: type(True) is bool, not int

✅ Better

value = True
if isinstance(value, bool):
    print("It's a bool")    # checks the actual type first
elif isinstance(value, int):
    print("It's an int")

Why: type() reports only the exact type, so type(True) == int is False and the fragile version above prints nothing, even though True really is an integer under the hood. isinstance() respects the family tree, so isinstance(True, int) returns True. The takeaway: reach for isinstance() when you check types, and test the most specific type (bool) before the more general one (int).

Mistake 3: Confusing None with empty or zero

📄 none_vs_empty.py: these are all different

# These are ALL different
print(f"None == 0: {None == 0}")
print(f"None == '': {None == ''}")
print(f"None == False: {None == False}")
print(f"None == []: {None == []}")

# All falsy, but not the same thing
values = [None, 0, '', False, []]
for v in values:
    print(f"{str(v):>7} -> bool: {bool(v)}, type: {type(v).__name__}")

▶ Output

None == 0: False
None == '': False
None == False: False
None == []: False
   None -> bool: False, type: NoneType
      0 -> bool: False, type: int
        -> bool: False, type: str
  False -> bool: False, type: bool
     [] -> bool: False, type: list

Why: None, 0, "", False, and [] are all falsy, but they are NOT equal to each other. None means “no value assigned.” 0 means “the number zero.” "" means “an empty string.” They have different types and different meanings.

Best Practices

  • DO use type() to inspect types when debugging: print(type(x))
  • DO use isinstance() for type checking: isinstance(x, str)
  • DO use Decimal for money, not float
  • DO check None with is: if x is None
  • DO use underscores in large numbers: 1_000_000
  • DON’T assume 0.1 + 0.2 == 0.3, because it does not
  • DON’T use type(x) == str; use isinstance(x, str) instead
  • DON’T treat None as zero or empty, because they mean different things

Practice Exercises

  1. Exercise 1: Create one variable of each basic type and verify with type().
  2. Exercise 2: Demonstrate mutable vs immutable: try modifying a string character vs a list element.
  3. Exercise 3: Build a type-checker returning descriptions like “42 is an integer”. Test with 6+ types.

Conclusion

The built-in Python data types cover almost everything you need: int for whole numbers (with unlimited precision), float for decimals (watch the precision traps), str for text, bool for true/false (secretly an integer), and None for “nothing.” The less common types (complex, bytes, bytearray) are there when you need them.

The mutability distinction is the most important concept from this post. Immutable types (int, float, str, tuple, bool, bytes) can never be changed after creation. Mutable types (list, dict, set, bytearray) can. This single distinction explains most of the “weird” behavior beginners hit when working with Python data types.

Next up: Type Conversion, how to convert between these types, what’s implicit vs explicit, and the catches that trip everyone up.

And if you want the full roadmap, from these fundamentals all the way to AI and machine learning projects, head over to the Python + AI/ML tutorial series home and pick your next post.

Frequently Asked Questions

What are the main data types in Python?

The built-in Python data types fall into 8 categories: numeric (int, float, complex, bool), text (str), sequence (list, tuple, range), binary (bytes, bytearray), mapping (dict), set (set, frozenset), and special (NoneType).

What is the difference between int and float in Python?

int stores whole numbers with unlimited precision (no decimal point). float stores decimal numbers using 64-bit IEEE 754 format, which means it has precision limits, so 0.1 + 0.2 is not exactly 0.3. Use int for counting and indexing, float for measurements and calculations.

Is bool a subclass of int in Python?

Yes. bool inherits from int in Python. True equals 1 and False equals 0. You can use booleans in arithmetic: True + True returns 2. This is by design, not a bug, and it lets booleans slot right into numeric contexts.

What is the difference between None, 0, and empty string in Python?

None means no value assigned (type NoneType). 0 is the integer zero (type int). '' is an empty string (type str). All three are falsy (evaluate to False in boolean context), but they are not equal to each other and have different types and meanings.

What is the difference between bytes and str in Python?

str holds Unicode text (human-readable characters). bytes holds raw binary data (sequences of integers 0-255). Convert between them with .encode() (str to bytes) and .decode() (bytes to str). When reading files in binary mode or handling network data, you work with bytes.

Why does 0.1 + 0.2 not equal 0.3 in Python?

This is not a Python bug. It is how all computers store decimal numbers in binary (the IEEE 754 standard). The number 0.1 cannot be represented exactly in binary, similar to how 1/3 cannot be represented exactly in decimal. For exact decimal arithmetic, use from decimal import Decimal.

Interview Questions on Python Data Types

Scenario questions, not trivia: this is the form this topic takes in a real interview.

Q: What is the difference between mutable and immutable types in Python? Give an example of each.

Immutable types (int, float, str, bool, tuple, bytes) can never be changed after creation: calling "aditi".upper() returns a brand new string and leaves the original untouched. Mutable types (list, dict, set, bytearray) can be modified in place: scores.append(95) changes the same object, and id(scores) stays identical before and after. The distinction matters because two variables can point to the same mutable object, so changing it through one name changes what both names see.

Q: Why should you prefer isinstance() over comparing type() with ==?

type(x) == int checks only the exact type and ignores inheritance, so it fails for subclasses: type(True) == int is False even though bool inherits from int. isinstance(True, int) returns True because it respects the class hierarchy. As a bonus, isinstance() accepts a tuple of types, like isinstance(x, (int, float)), which makes numeric checks much cleaner.

Q: Scenario: your billing script stores prices as floats, and after thousands of invoices the totals are off by a few paise. What is going wrong and how do you fix it?

Floats are binary approximations, so values like 19.99 and 0.1 are not stored exactly, and every arithmetic step leaks a tiny rounding error that accumulates across thousands of records. The fix is to switch to decimal.Decimal and construct values from strings, Decimal("19.99"), never from floats, so the arithmetic stays exact. This is the standard practice for money in Python.

Q: Scenario: your code crashes with AttributeError: ‘NoneType’ object has no attribute ‘upper’. What happened and what do you check first?

Some function returned None instead of a string, and you then called a string method on that None. First check the function whose result you used: if it has no return statement, or has a code path that skips return, Python silently returns None. Guard the call site with if result is None before using the value, and always compare against None with is, not ==.

Q: How is a Python int different from an int in C or Java?

Python ints have unlimited precision: 10 ** 100 just works, growing in memory as needed. C and Java ints are fixed-width (32 or 64 bits) and overflow or wrap when a value gets too large. The trade-off is that every Python int is a full object, so it uses more memory and is slower than a raw machine integer, which is exactly why libraries like NumPy fall back to fixed-width types for speed.

Q: Scenario: a form field where 0 is a valid answer uses “if value:” to detect whether the user answered, and valid zeros get treated as “no answer”. Why, and what is the correct check?

In Python, 0, 0.0, "", [], and None are all falsy, so if value: cannot tell “the user entered zero” apart from “no value at all”. When zero is legitimate data, the correct check is if value is not None:, which tests for absence specifically instead of general truthiness. Reserve plain if value: for cases where every falsy value really does mean “nothing to do”.

Q: You index into b”Hello” with data[0] and get 72 instead of ‘H’. Why?

A bytes object is a sequence of integers from 0 to 255, so indexing returns the numeric byte value, and 72 is the ASCII code for ‘H’. If you want a one-byte bytes object instead, slice it: data[0:1] gives b'H'. And if you want readable text, decode the whole thing with data.decode("utf-8").

Go deeper: when you outgrow this post, the official Python documentation is the next stop.

Previous: Python Variables: Naming, Assignment, and the Memory Model

Next: Python: Type Conversion, Implicit vs Explicit with Catches

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *