Chapter 3 of 14

Python Data Types

Deep dive into Python's data types — integers, floats, strings, booleans, None, type conversion, and how Python manages types internally.

Meritshot29 min read
PythonData TypesStringsNumbersBoolean
All Python Chapters

Why Data Types Matter

Every piece of data your program works with has a type. When you write age = 25, Python doesn't just store the number — it stores metadata that says "this value is an integer." Understanding data types is fundamental for three reasons:

1. Type Safety and Correctness

Operations behave differently depending on the type of values involved. The + operator, for example, adds two numbers but concatenates two strings. If you accidentally mix types without converting, Python raises a TypeError:

# This works — both operands are the same type
print(10 + 5)           # 15
print("Hello" + " World")  # Hello World

# This fails — you can't add a string to an integer
# print("Age: " + 25)    # TypeError: can only concatenate str to str
print("Age: " + str(25))  # Age: 25 — explicit conversion fixes it

2. Memory and Performance

Different types consume different amounts of memory. An integer 1 and a string "1" are stored very differently internally. When you process millions of records, choosing the right type (e.g., using integers instead of strings for numeric IDs) can make your program significantly faster.

3. Debugging

A huge percentage of beginner bugs boil down to type mismatches — comparing a string "25" to an integer 25, or trying to do arithmetic on the result of input() (which always returns a string). Knowing your types is the fastest path to diagnosing these errors.

Core Data Types at a Glance

Python has several built-in data types. This chapter focuses on the scalar (single-value) types. Collection types like lists, tuples, dictionaries, and sets are covered in dedicated chapters.

CategoryTypeDescriptionExample
NumericintWhole numbers, unlimited precision42, -7, 0, 1_000_000
NumericfloatDecimal (floating-point) numbers3.14, -0.5, 1.0e6
NumericcomplexComplex numbers with real and imaginary parts3+4j, complex(3, 4)
TextstrImmutable sequences of characters"hello", 'world', """block"""
BooleanboolLogical truth valuesTrue, False
NoneNoneTypeRepresents the absence of a valueNone

Numeric Types in Detail

Integers (int)

Python integers have unlimited precision — they can be as large or as small as your machine's memory allows. There is no overflow like in C or Java where int is limited to 32 or 64 bits.

# Regular integers
x = 42
y = -17
z = 0

# Very large integers — Python handles them natively
big = 99999999999999999999999999999999
print(big + 1)  # 100000000000000000000000000000000

# Use underscores for readability (Python 3.6+)
population = 1_400_000_000
budget = 2_50_000            # Indian numbering style works too
print(population)            # 1400000000

Integer Bases

You can write integers in binary, octal, or hexadecimal using prefixes:

# Binary (base 2) — prefix 0b or 0B
binary_val = 0b1010
print(binary_val)       # 10

# Octal (base 8) — prefix 0o or 0O
octal_val = 0o17
print(octal_val)        # 15

# Hexadecimal (base 16) — prefix 0x or 0X
hex_val = 0xFF
print(hex_val)          # 255

# Converting integers TO different base strings
print(bin(255))         # 0b11111111
print(oct(255))         # 0o377
print(hex(255))         # 0xff

# Converting base strings BACK to integers
print(int("1010", 2))   # 10 — binary to int
print(int("17", 8))     # 15 — octal to int
print(int("FF", 16))    # 255 — hex to int

Useful Built-in Functions for Integers

# abs() — absolute value
print(abs(-42))          # 42
print(abs(42))           # 42

# pow() — exponentiation (same as **)
print(pow(2, 10))        # 1024
print(2 ** 10)           # 1024

# pow() with three arguments — modular exponentiation (efficient)
# pow(base, exp, mod) computes (base ** exp) % mod
print(pow(2, 10, 1000))  # 24  (i.e., 1024 % 1000)

# divmod() — returns (quotient, remainder) as a tuple
print(divmod(17, 5))     # (3, 2) — because 17 = 5*3 + 2
print(divmod(100, 7))    # (14, 2)

# Practical use of divmod: converting seconds to minutes and seconds
total_seconds = 3725
minutes, seconds = divmod(total_seconds, 60)
hours, minutes = divmod(minutes, 60)
print(f"{hours}h {minutes}m {seconds}s")  # 1h 2m 5s

Floats (float)

Floats represent decimal numbers. Internally, Python uses the IEEE 754 double-precision format (64-bit), which gives you roughly 15-17 significant decimal digits of precision.

# Creating floats
pi = 3.14159
temperature = -40.0
tiny = 0.000001

# Scientific notation (e or E)
speed_of_light = 3.0e8       # 300,000,000.0
planck = 6.626e-34            # 0.0000000000000000000000000000000006626

# A whole number becomes a float if you add a decimal point
x = 10      # int
y = 10.0    # float
print(type(x))  # <class 'int'>
print(type(y))  # <class 'float'>

Floating-Point Precision Issues

This is one of the most important things to understand in any programming language. Floats are stored in binary, and many decimal fractions (like 0.1) have no exact binary representation. This leads to tiny rounding errors:

# The classic surprise
print(0.1 + 0.2)            # 0.30000000000000004
print(0.1 + 0.2 == 0.3)     # False!

# More precision issues
print(0.1 + 0.1 + 0.1 - 0.3)  # 5.551115123125783e-17 (not exactly 0)

How to handle this:

import math

# Option 1: math.isclose() — recommended for comparisons
print(math.isclose(0.1 + 0.2, 0.3))  # True

# You can control the tolerance
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9))  # True

# Option 2: round() for display purposes
result = 0.1 + 0.2
print(round(result, 1))     # 0.3

# Option 3: decimal module for exact decimal arithmetic
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))  # 0.3 (exact)
# IMPORTANT: Use strings with Decimal, not floats
# Decimal(0.1) still has the imprecision — always use Decimal("0.1")

Rule of thumb: Never compare floats with ==. Use math.isclose() or round to a fixed number of decimal places. For financial calculations, use the decimal module.

Special Float Values

Python supports three special float values:

# Positive infinity
pos_inf = float('inf')
print(pos_inf > 999999999999)  # True
print(pos_inf + 1)              # inf
print(pos_inf + pos_inf)        # inf

# Negative infinity
neg_inf = float('-inf')
print(neg_inf < -999999999999)  # True

# Not a Number (NaN) — represents undefined results
nan = float('nan')
print(nan == nan)               # False! NaN is not equal to itself
print(nan != nan)               # True

# Use math.isnan() and math.isinf() to check
import math
print(math.isnan(nan))          # True
print(math.isinf(pos_inf))     # True
print(math.isfinite(3.14))     # True
print(math.isfinite(pos_inf))  # False

Complex Numbers (complex)

Python has built-in support for complex numbers. You'll encounter these mainly in scientific computing, signal processing, or electrical engineering. For everyday programming, you can safely skip this section.

# Creating complex numbers — use j (not i) for the imaginary part
z = 3 + 4j
print(z)            # (3+4j)
print(type(z))      # <class 'complex'>

# Accessing parts
print(z.real)       # 3.0
print(z.imag)       # 4.0

# Using the constructor
z2 = complex(3, 4)  # same as 3 + 4j

# Arithmetic
print(z + z2)       # (6+8j)
print(abs(z))       # 5.0 — magnitude (sqrt(3^2 + 4^2))

Common Numeric Functions

These functions work across numeric types:

# round() — round to n decimal places
print(round(3.14159))       # 3 (rounds to nearest int)
print(round(3.14159, 2))    # 3.14
print(round(3.145, 2))      # 3.14 (banker's rounding — rounds to even)
print(round(3.155, 2))      # 3.15
print(round(2.5))           # 2 (banker's rounding — not 3!)
print(round(3.5))           # 4

# abs() — absolute value
print(abs(-7))              # 7
print(abs(-3.14))           # 3.14

# min() and max()
print(min(3, 1, 4, 1, 5))  # 1
print(max(3, 1, 4, 1, 5))  # 5

# Also work with iterables
numbers = [23, 45, 12, 67, 34]
print(min(numbers))         # 12
print(max(numbers))         # 67

# sum() — add all elements
print(sum([1, 2, 3, 4, 5]))       # 15
print(sum([1, 2, 3], 10))         # 16 (start value = 10)
print(sum(range(1, 101)))         # 5050 (sum of 1 to 100)
FunctionDescriptionExampleResult
abs(x)Absolute valueabs(-5)5
round(x, n)Round to n decimal placesround(3.14159, 2)3.14
pow(x, y)x raised to power ypow(2, 10)1024
divmod(x, y)Quotient and remainderdivmod(17, 5)(3, 2)
min(...)Smallest valuemin(3, 1, 4)1
max(...)Largest valuemax(3, 1, 4)4
sum(iterable)Sum of all elementssum([1, 2, 3])6

Strings in Detail

Strings are sequences of Unicode characters. This section gives a thorough preview — strings have their own dedicated chapter later with even more depth on methods, regular expressions, and encoding.

Creating Strings

# Single quotes
name = 'Meritshot'

# Double quotes — functionally identical
name = "Meritshot"

# Use single quotes inside double, or vice versa
message = "It's a beautiful day"
html = '<div class="container">Hello</div>'

# Triple quotes — for multi-line strings
bio = """This is a
multi-line string.
It preserves line breaks."""

# Triple single quotes work too
poem = '''Roses are red,
Violets are blue,
Python is awesome,
And so are you.'''

print(bio)
# Output:
# This is a
# multi-line string.
# It preserves line breaks.

Escape Characters

When you need special characters inside a string, use the backslash \:

# Common escape characters
print("Hello\nWorld")     # newline — prints on two lines
print("Name:\tPriya")     # tab
print("She said \"Hi\"")  # escaped double quote
print('It\'s fine')       # escaped single quote
print("C:\\Users\\docs")  # escaped backslash

# Unicode characters
print("\u2764")            # prints a heart symbol
print("\u20B9")            # ₹ (Indian Rupee sign)
EscapeDescriptionExampleOutput
\nNewline"Line1\nLine2"Line1 (newline) Line2
\tTab"Col1\tCol2"Col1 (tab) Col2
\\Literal backslash"C:\\path"C:\path
\"Double quote"say \"hi\""say "hi"
\'Single quote'it\'s'it's
\0Null character"end\0here"end (null) here

Raw Strings

Prefix a string with r to disable escape character processing. This is very useful for Windows file paths and regular expressions:

# Without raw string — \n is interpreted as newline
path = "C:\new_folder\test"
print(path)
# C:
# ew_folder	est    (oops!)

# With raw string — backslashes are literal
path = r"C:\new_folder\test"
print(path)  # C:\new_folder\test

# Useful for regex patterns
import re
pattern = r"\d{3}-\d{4}"   # matches 123-4567

Immutability

Strings in Python are immutable — once created, you cannot change individual characters. Any operation that "modifies" a string actually creates a new one:

name = "Python"

# This will NOT work
# name[0] = "J"    # TypeError: 'str' object does not support item assignment

# Instead, create a new string
new_name = "J" + name[1:]
print(new_name)    # Jython

# String methods also return NEW strings
greeting = "hello"
upper_greeting = greeting.upper()
print(greeting)        # hello — original unchanged
print(upper_greeting)  # HELLO — new string

Indexing and Slicing

Strings are sequences, so you can access individual characters by position (index) and extract substrings using slicing:

text = "Meritshot"
#       M  e  r  i  t  s  h  o  t
# Index 0  1  2  3  4  5  6  7  8
# Neg  -9 -8 -7 -6 -5 -4 -3 -2 -1

# Indexing — access a single character
print(text[0])     # M (first character)
print(text[5])     # s
print(text[-1])    # t (last character)
print(text[-3])    # h (third from end)

# Slicing — extract a substring [start:stop:step]
# start is inclusive, stop is exclusive
print(text[0:5])   # Merit
print(text[5:9])   # shot
print(text[:5])    # Merit (start defaults to 0)
print(text[5:])    # shot (stop defaults to end)
print(text[:])     # Meritshot (full copy)

# Step (every nth character)
print(text[::2])   # Mrsto (every 2nd character)
print(text[::-1])  # tohstireM (reversed!)

# Practical: extract file extension
filename = "report_2026.pdf"
extension = filename[filename.rfind("."):]   # .pdf

Common String Methods

Python strings come with dozens of built-in methods. Here are the most important ones:

text = "  Hello, World!  "

# --- Case Methods ---
print(text.upper())         # "  HELLO, WORLD!  "
print(text.lower())         # "  hello, world!  "
print(text.title())         # "  Hello, World!  "
print(text.capitalize())    # "  hello, world!  " (only first char)
print(text.swapcase())      # "  hELLO, wORLD!  "

# --- Whitespace Methods ---
print(text.strip())         # "Hello, World!" (removes leading/trailing whitespace)
print(text.lstrip())        # "Hello, World!  " (left only)
print(text.rstrip())        # "  Hello, World!" (right only)

# --- Search Methods ---
sentence = "Python is powerful and Python is popular"
print(sentence.find("Python"))      # 0 (first occurrence index)
print(sentence.find("Python", 5))   # 25 (start searching from index 5)
print(sentence.find("Java"))        # -1 (not found)
print(sentence.count("Python"))     # 2
print(sentence.startswith("Python"))  # True
print(sentence.endswith("popular"))   # True

# --- Replace and Split ---
print(sentence.replace("Python", "Java"))
# "Java is powerful and Java is popular"

print(sentence.replace("Python", "Java", 1))
# "Java is powerful and Python is popular" (replace only first)

words = "apple,banana,cherry"
print(words.split(","))     # ['apple', 'banana', 'cherry']
print(words.split(",", 1))  # ['apple', 'banana,cherry'] (split once)

# Default split() splits on any whitespace and removes empty strings
messy = "  Hello   World   Python  "
print(messy.split())        # ['Hello', 'World', 'Python']

# --- Join (the reverse of split) ---
fruits = ['apple', 'banana', 'cherry']
print(", ".join(fruits))     # "apple, banana, cherry"
print(" -> ".join(fruits))   # "apple -> banana -> cherry"
print("\n".join(fruits))     # each on a new line

# --- Checking Content ---
print("12345".isdigit())     # True
print("hello".isalpha())     # True
print("hello123".isalnum())  # True
print("   ".isspace())       # True
MethodDescriptionExampleResult
upper()All uppercase"hi".upper()"HI"
lower()All lowercase"HI".lower()"hi"
strip()Remove whitespace" hi ".strip()"hi"
replace(a, b)Replace substring"hi".replace("h","b")"bi"
split(sep)Split into list"a,b".split(",")["a","b"]
join(list)Join list into string",".join(["a","b"])"a,b"
find(sub)Index of substring"hello".find("ll")2
count(sub)Count occurrences"hello".count("l")2
startswith(s)Starts with prefix"hello".startswith("he")True
endswith(s)Ends with suffix"hello".endswith("lo")True

f-Strings (Formatted String Literals)

f-strings (Python 3.6+) are the modern, preferred way to embed expressions inside strings. Prefix the string with f and put expressions inside {}:

name = "Priya"
score = 95.678
items = 1_250_000

# Basic variable insertion
print(f"Hello, {name}!")            # Hello, Priya!

# Expressions inside braces
print(f"2 + 3 = {2 + 3}")          # 2 + 3 = 5
print(f"Name length: {len(name)}") # Name length: 5

# --- Number Formatting ---

# Fixed decimal places
print(f"Score: {score:.2f}")        # Score: 95.68

# Thousands separator with comma
print(f"Items: {items:,}")          # Items: 1,250,000

# Combining comma separator and decimal places
salary = 1250000.5
print(f"Salary: ₹{salary:,.2f}")   # Salary: ₹1,250,000.50

# Percentage
ratio = 0.856
print(f"Accuracy: {ratio:.1%}")     # Accuracy: 85.6%

# Padding with zeros
order_id = 42
print(f"Order #{order_id:05d}")     # Order #00042

# --- Alignment and Padding ---

# Left-aligned (default for strings)
print(f"{'Name':<20}{'Score':>10}")
print(f"{'Priya':<20}{95:>10}")
print(f"{'Rahul':<20}{87:>10}")
# Output:
# Name                     Score
# Priya                       95
# Rahul                       87

# Center-aligned
print(f"{'REPORT':=^40}")
# =================REPORT=================

# --- Advanced: Expressions and Method Calls ---
data = [85, 92, 78, 95, 88]
print(f"Average: {sum(data)/len(data):.1f}")  # Average: 87.6
print(f"Name: {name.upper()!r}")              # Name: 'PRIYA'
Format SpecDescriptionExampleResult
:.2f2 decimal placesf"{3.14159:.2f}"3.14
:,Thousands separatorf"{1000000:,}"1,000,000
:.1%Percentage (1 dp)f"{0.85:.1%}"85.0%
:05dZero-padded integerf"{42:05d}"00042
:<20Left-align, width 20f"{'hi':<20}"hi
:>20Right-align, width 20f"{'hi':>20}" hi
:^20Center, width 20f"{'hi':^20}" hi

String Multiplication and Concatenation

# Concatenation with +
first = "Merit"
last = "shot"
full = first + last
print(full)              # Meritshot

# Repetition with *
line = "-" * 40
print(line)              # ----------------------------------------

# Practical: creating a simple banner
title = "Python Data Types"
border = "=" * (len(title) + 4)
print(border)
print(f"| {title} |")
print(border)
# =====================
# | Python Data Types |
# =====================

# Concatenation in a loop (INEFFICIENT for many items)
# Use join() instead:
words = ["Python", "is", "awesome"]
sentence = " ".join(words)   # "Python is awesome" — much faster

Booleans

Booleans represent one of two values: True or False. They are the foundation of all decision-making in your programs.

Basics

is_active = True
is_deleted = False

print(type(is_active))    # <class 'bool'>

# Booleans from comparisons
print(10 > 5)       # True
print(3 == 7)       # False
print("a" != "b")   # True
print(5 >= 5)       # True

Booleans Are Integers

In Python, bool is a subclass of int. True is literally 1 and False is literally 0:

print(True == 1)       # True
print(False == 0)      # True
print(True + True)     # 2
print(True * 10)       # 10
print(False + 5)       # 5

# This means you can use booleans in arithmetic
# Practical: counting True values in a list
results = [True, False, True, True, False]
print(sum(results))    # 3 (counts how many are True)

# isinstance confirms bool is a subclass of int
print(isinstance(True, int))   # True
print(isinstance(True, bool))  # True

Truthy and Falsy Values

In Python, every value has a boolean interpretation. When used in a boolean context (like an if statement), values are either "truthy" (treated as True) or "falsy" (treated as False).

Falsy values (everything else is truthy):

TypeFalsy Value
boolFalse
NoneTypeNone
int0
float0.0
complex0j
str"" (empty string)
list[] (empty list)
tuple() (empty tuple)
dict{} (empty dict)
setset() (empty set)
rangerange(0) (empty range)
# Demonstrating truthy/falsy behaviour
if "hello":
    print("Non-empty string is truthy")    # This prints

if "":
    print("This won't print")             # Skipped

if 42:
    print("Non-zero number is truthy")     # This prints

if 0:
    print("This won't print")             # Skipped

if [1, 2, 3]:
    print("Non-empty list is truthy")      # This prints

if []:
    print("This won't print")             # Skipped

# Practical: check if a list has items
shopping_list = ["milk", "eggs"]
if shopping_list:
    print(f"You have {len(shopping_list)} items to buy")
else:
    print("Your shopping list is empty")

The bool() Function

You can explicitly convert any value to a boolean using bool():

# Numbers
print(bool(0))        # False
print(bool(0.0))      # False
print(bool(42))       # True
print(bool(-1))       # True (any non-zero number)

# Strings
print(bool(""))       # False
print(bool("hello"))  # True
print(bool(" "))      # True (a space is NOT empty)
print(bool("False"))  # True (the STRING "False" is non-empty!)

# Collections
print(bool([]))       # False
print(bool([0]))      # True (list is not empty, even though element is 0)

# None
print(bool(None))     # False

Common gotcha: bool("False") is True because the string "False" is non-empty. To convert the string "False" to the boolean False, you need custom logic.


NoneType

What is None?

None is Python's way of representing "nothing" or "no value." It is the sole instance of the NoneType class and is a singleton — there is only ever one None object in memory.

result = None
print(result)       # None
print(type(result)) # <class 'NoneType'>

When You'll Encounter None

# 1. Default function return value
def greet(name):
    print(f"Hello, {name}!")
    # No explicit return statement

result = greet("Priya")
print(result)    # None — functions return None by default

# 2. Initialising a variable that will be assigned later
user_input = None
best_score = None

# 3. Optional function parameters
def find_user(name, age=None):
    if age is None:
        print(f"Searching for {name} (any age)")
    else:
        print(f"Searching for {name}, age {age}")

find_user("Priya")          # Searching for Priya (any age)
find_user("Priya", 25)      # Searching for Priya, age 25

# 4. Indicating missing data
data = {"name": "Priya", "email": None}
# email is explicitly set to None — meaning "no email provided"

is None vs == None

Always use is None (or is not None) rather than == None. The is operator checks identity (same object in memory), which is both faster and more correct:

x = None

# Preferred — identity check
if x is None:
    print("x is None")

if x is not None:
    print("x has a value")

# Avoid — equality check (can be overridden by custom classes)
if x == None:    # works, but not recommended
    print("x is None")

The reason is is preferred: a custom class could override the __eq__ method and make some_object == None return True even when the object is not actually None. The is operator cannot be overridden and always checks true identity.


Type Checking

The type() Function

type() returns the exact type of a value:

print(type(42))         # <class 'int'>
print(type(3.14))       # <class 'float'>
print(type("hello"))    # <class 'str'>
print(type(True))       # <class 'bool'>
print(type(None))       # <class 'NoneType'>
print(type([1, 2]))     # <class 'list'>

# You can compare types directly
x = 42
if type(x) == int:
    print("x is an integer")

The isinstance() Function (Preferred)

isinstance() checks if a value is an instance of a given type or any of its subclasses. This makes it more flexible and Pythonic:

x = 42

# Basic usage
print(isinstance(x, int))    # True
print(isinstance(x, float))  # False
print(isinstance(x, str))    # False

# Check against multiple types at once (tuple of types)
print(isinstance(x, (int, float)))  # True — x is an int OR float

# Works with inheritance
print(isinstance(True, int))   # True — bool is a subclass of int
print(type(True) == int)       # False — type() checks exact type only

Why isinstance() is Better Than type()

# Example: bool is a subclass of int
value = True

# type() — too strict, misses the relationship
if type(value) == int:
    print("It's an int")       # Does NOT print
if type(value) == bool:
    print("It's a bool")      # Prints

# isinstance() — respects inheritance
if isinstance(value, int):
    print("It's an int")       # Prints (because bool IS an int)
if isinstance(value, bool):
    print("It's a bool")      # Also prints

# Practical: accepting both int and float
def calculate_tax(amount):
    if not isinstance(amount, (int, float)):
        raise TypeError("amount must be a number")
    return amount * 0.18

print(calculate_tax(1000))    # 180.0
print(calculate_tax(99.99))   # 17.9982

Type Conversion (Casting)

Type conversion is the process of converting a value from one type to another. Python supports both implicit (automatic) and explicit (manual) conversion.

Implicit Conversion

Python automatically converts types when it can do so without losing data:

# int + float = float (int is "promoted" to float)
result = 10 + 3.14
print(result)          # 13.14
print(type(result))    # <class 'float'>

# int + bool = int (True becomes 1, False becomes 0)
result = 10 + True
print(result)          # 11

# Python does NOT implicitly convert between str and numbers
# print("Age: " + 25)  # TypeError!

Explicit Conversion

Use built-in functions to manually convert between types:

# --- int() ---
print(int(3.99))        # 3 (truncates toward zero, does NOT round)
print(int(-3.99))       # -3
print(int("42"))        # 42
print(int("0b1010", 2)) # 10 (binary string)
print(int(True))        # 1
print(int(False))       # 0

# --- float() ---
print(float(42))        # 42.0
print(float("3.14"))    # 3.14
print(float("inf"))     # inf
print(float(True))      # 1.0

# --- str() ---
print(str(42))          # "42"
print(str(3.14))        # "3.14"
print(str(True))        # "True"
print(str(None))        # "None"
print(str([1, 2, 3]))   # "[1, 2, 3]"

# --- bool() ---
print(bool(0))          # False
print(bool(42))         # True
print(bool(""))         # False
print(bool("hello"))    # True
print(bool([]))         # False
print(bool(None))       # False

# --- Collection conversions (preview) ---
print(list("hello"))         # ['h', 'e', 'l', 'l', 'o']
print(tuple([1, 2, 3]))     # (1, 2, 3)
print(set([1, 2, 2, 3]))    # {1, 2, 3} (duplicates removed)
print(list(range(5)))        # [0, 1, 2, 3, 4]

Common Conversion Pitfalls

# PITFALL 1: int() cannot parse a decimal string directly
# print(int("3.14"))     # ValueError: invalid literal for int()
# Fix: convert to float first, then to int
print(int(float("3.14")))   # 3

# PITFALL 2: int() truncates, it doesn't round
print(int(9.9))             # 9 (not 10)
print(round(9.9))           # 10 (use round() if you want rounding)

# PITFALL 3: converting non-numeric strings to numbers
# print(int("hello"))    # ValueError
# print(float("abc"))    # ValueError

# Safe conversion using try/except
def safe_int(value):
    """Convert to int safely, return None on failure."""
    try:
        return int(value)
    except (ValueError, TypeError):
        return None

print(safe_int("42"))       # 42
print(safe_int("hello"))    # None
print(safe_int(None))       # None

# PITFALL 4: str(list) gives a string representation, not elements
my_list = [1, 2, 3]
print(str(my_list))          # "[1, 2, 3]" — a string, not usable as list
# To join list elements into a string:
print(", ".join(str(x) for x in my_list))  # "1, 2, 3"

Conversion Reference Table

FromTo intTo floatTo strTo bool
int 424242.0"42"True
int 000.0"0"False
float 3.143 (truncated)3.14"3.14"True
float 0.000.0"0.0"False
str "42"4242.0"42"True
str ""ValueErrorValueError""False
bool True11.0"True"True
bool False00.0"False"False
NoneTypeErrorTypeError"None"False

Dynamic Typing Explained

Variables Are Labels, Not Boxes

In many languages (like C or Java), a variable is a named memory location (a "box") that holds a value of a fixed type. In Python, variables work differently. A variable is a label (or reference) that points to an object in memory. The type belongs to the object, not the variable.

x = 42           # x points to an int object 42
print(type(x))   # <class 'int'>

x = "hello"      # now x points to a str object "hello"
print(type(x))   # <class 'str'>

x = [1, 2, 3]    # now x points to a list object
print(type(x))   # <class 'list'>

# The variable x has no fixed type —
# it's just a name that can refer to any object

The id() Function

Every object in Python has a unique identifier (its memory address). The id() function reveals it:

a = 42
b = 42
print(id(a))      # e.g., 140712345678
print(id(b))      # same as id(a)!
print(a is b)     # True — they point to the same object

# Python caches small integers (-5 to 256) and short strings
# for efficiency (this is called "interning")

c = 1000
d = 1000
print(c is d)     # Might be True or False depending on context

# Reassignment creates a new object (for immutable types)
x = 10
print(id(x))      # e.g., 140712345000
x = 20
print(id(x))      # different — x now points to a new object

Mutable vs Immutable Types (Preview)

This concept is covered in depth in the collections chapters, but here is the key distinction:

Immutable types — cannot be changed after creation. Any "modification" creates a new object.

  • int, float, complex, str, tuple, bool, frozenset, None

Mutable types — can be changed in place. The same object is modified.

  • list, dict, set, bytearray
# Immutable — strings
name = "Priya"
# name[0] = "K"  # TypeError — can't change a string in place

# Mutable — lists
scores = [90, 85, 92]
scores[0] = 95          # This works — lists are mutable
print(scores)           # [95, 85, 92]

# Why this matters: aliasing
a = [1, 2, 3]
b = a               # b points to the SAME list object
b.append(4)
print(a)             # [1, 2, 3, 4] — a is also changed!

# For immutable types, this doesn't happen
a = "hello"
b = a
b = b.upper()        # creates a new string
print(a)             # "hello" — unchanged
print(b)             # "HELLO"

Practical Examples

Example 1: Number Formatting for Currency

def format_currency(amount, currency="INR"):
    """Format a number as currency with proper symbols and grouping."""
    symbols = {
        "INR": "₹",
        "USD": "$",
        "EUR": "€",
        "GBP": "£"
    }

    symbol = symbols.get(currency, currency + " ")

    if amount < 0:
        return f"-{symbol}{abs(amount):,.2f}"
    return f"{symbol}{amount:,.2f}"

# Usage
print(format_currency(1250000))          # ₹1,250,000.00
print(format_currency(99.99, "USD"))     # $99.99
print(format_currency(-500, "EUR"))      # -€500.00
print(format_currency(1499.9, "GBP"))    # £1,499.90

Example 2: Parsing User Input with Error Handling

def get_number(prompt, number_type=float):
    """
    Safely get a number from the user.
    Keeps asking until a valid number is entered.
    """
    while True:
        user_input = input(prompt)

        # Allow the user to quit
        if user_input.lower() in ("q", "quit", "exit"):
            return None

        try:
            return number_type(user_input)
        except ValueError:
            print(f"  Invalid input: '{user_input}' is not a valid number.")
            print(f"  Please enter a valid {number_type.__name__}.")

# Usage
# age = get_number("Enter your age: ", int)
# if age is not None:
#     print(f"You are {age} years old")
# else:
#     print("Goodbye!")

# Example of how the interaction would look:
# Enter your age: abc
#   Invalid input: 'abc' is not a valid number.
#   Please enter a valid int.
# Enter your age: 25.5
#   Invalid input: '25.5' is not a valid number.
#   Please enter a valid int.
# Enter your age: 25
# You are 25 years old

Example 3: Data Validation Function

def validate_user_data(name, age, email, score):
    """
    Validate user data and return a dictionary of errors.
    Returns an empty dict if all validations pass.
    """
    errors = {}

    # Validate name — must be a non-empty string
    if not isinstance(name, str) or not name.strip():
        errors["name"] = "Name must be a non-empty string"

    # Validate age — must be a positive integer
    if not isinstance(age, int) or isinstance(age, bool):
        errors["age"] = "Age must be an integer"
    elif age < 0 or age > 150:
        errors["age"] = "Age must be between 0 and 150"

    # Validate email — basic check (must contain @ and .)
    if not isinstance(email, str):
        errors["email"] = "Email must be a string"
    elif "@" not in email or "." not in email.split("@")[-1]:
        errors["email"] = "Email must be a valid format (user@domain.com)"

    # Validate score — must be a number between 0 and 100
    if not isinstance(score, (int, float)) or isinstance(score, bool):
        errors["score"] = "Score must be a number"
    elif score < 0 or score > 100:
        errors["score"] = "Score must be between 0 and 100"

    return errors

# Test cases
print(validate_user_data("Priya", 22, "priya@example.com", 95.5))
# {} — all valid

print(validate_user_data("", -5, "invalid", 150))
# {'name': 'Name must be a non-empty string',
#  'age': 'Age must be between 0 and 150',
#  'email': 'Email must be a valid format (user@domain.com)',
#  'score': 'Score must be between 0 and 100'}

print(validate_user_data("Rahul", 25, "rahul@mail.com", 87))
# {} — all valid

# Demonstrating the isinstance + bool edge case:
# isinstance(True, int) is True, so we explicitly exclude booleans
print(validate_user_data("Test", True, "a@b.c", False))
# {'age': 'Age must be an integer', 'score': 'Score must be a number'}

Practice Exercises

Test your understanding of Python data types with these exercises. Try to solve them yourself before looking at hints.

Exercise 1: Type Inspector

Write a function inspect_value(value) that takes any value and prints its type, its truthiness, and its string representation. Example output for inspect_value(42):

Value: 42
Type:  <class 'int'>
Truthy: True
str(): '42'

Exercise 2: Floating-Point Detective

Write code that demonstrates the floating-point precision issue. Create a list of expressions where the mathematical result differs from Python's actual result. Use math.isclose() to verify the correct comparisons. Try at least three different examples beyond 0.1 + 0.2.

Exercise 3: Base Converter

Write a function convert_base(number, base) that takes an integer and a target base (2, 8, or 16) and returns the string representation. If an invalid base is provided, return an error message. Include the appropriate prefix (0b, 0o, 0x).

Exercise 4: Smart Type Caster

Write a function smart_cast(value) that takes a string and automatically converts it to the most appropriate type:

  • "42" should return int 42
  • "3.14" should return float 3.14
  • "True" or "False" should return bool
  • "None" should return None
  • Anything else stays as a string

Exercise 5: Receipt Formatter

Write a function that takes a list of tuples (item_name, quantity, unit_price) and prints a formatted receipt using f-strings. Include proper column alignment, subtotals per item, and a grand total. Example input:

items = [
    ("Notebook", 3, 45.00),
    ("Pen", 10, 12.50),
    ("Eraser", 5, 8.00),
    ("Ruler", 2, 35.00),
]

Expected output should have columns for item, quantity, price, and subtotal, all neatly aligned.

Exercise 6: Type Validation Decorator

(Advanced) Write a function validate_types(value, expected_types) that:

  • Returns True if the value matches any of the expected types
  • Handles the bool/int subclass quirk correctly (i.e., True should NOT pass as a valid int unless bool is also in the expected types)
  • Works with None as an expected type

Summary

In this chapter, you took a deep dive into Python's data types:

  • Numeric typesint (unlimited precision, multiple bases), float (IEEE 754 with precision caveats), and complex (real + imaginary)
  • Strings — creation with different quote styles, escape characters, raw strings, immutability, indexing, slicing, common methods, and f-string formatting
  • BooleansTrue/False, their relationship to integers, and the concept of truthy/falsy values
  • NoneType — what None represents, when to use it, and why is None is preferred over == None
  • Type checkingtype() vs isinstance() and why the latter is more Pythonic
  • Type conversion — implicit vs explicit casting, conversion functions, and common pitfalls like int("3.14") failing
  • Dynamic typing — variables as labels, the id() function, and the mutable vs immutable distinction

Key takeaways to remember:

  • Never compare floats with == — use math.isclose()
  • Always use is None instead of == None
  • Prefer isinstance() over type() for type checking
  • Be aware of truthy/falsy values — empty collections and zero are falsy
  • input() always returns a string — convert explicitly when you need numbers

Next up: Conditional Statements — learn how to make decisions in your programs with if, elif, else, and write logic that responds to different situations.