Data Types #
Every value in Python has a type — and the type determines what operations can be performed on that value. Python is strongly typed: there’s no implicit conversion between incompatible types. You can’t directly add a number to a string like you can in JavaScript. But Python is also dynamically typed: types are determined at runtime, not at compile time. Understanding data types well — including which are mutable and which are immutable, how conversion works, and the non-intuitive traps — is an essential foundation before writing more complex Python code.
The Python Data Type Map #
To make things easier to map out, here’s a visualization of Python’s built-in data type hierarchy, split into scalar types (single values) and collection types (groups of values), along with their mutability grouping:
flowchart TD
Root["Python Data Types"] --> Scalar["Scalar Types (Single Value)"]
Root --> Collection["Collection Types (Group of Values)"]
subgraph Skalar ["Scalar Category"]
Scalar --> int["int<br/>'(Integer)'"]
Scalar --> float["float<br/>'(Decimal)'"]
Scalar --> complex["complex<br/>'(Complex Number)'"]
Scalar --> bool["bool<br/>'(Boolean)'"]
Scalar --> NoneType["NoneType<br/>'(None)'"]
end
subgraph Koleksi ["Collection Category"]
Collection --> Ordered["Ordered"]
Collection --> Unordered["Unordered"]
Ordered -->|Mutable| list["list<br/>'[1, 2, 3]'"]
Ordered -->|Immutable| tuple["tuple<br/>'(1, 2, 3)'"]
Ordered -->|Immutable| str["str<br/>'\"hello\"'"]
Unordered -->|Mutable| dict["dict<br/>'{\"key\": \"value\"}'"]
Unordered -->|Mutable| set["set<br/>'{1, 2, 3}'"]
Unordered -->|Immutable| frozenset["frozenset<br/>'({1, 2, 3})'"]
endint — Integers
#
Python’s int has no size limit — it can hold integers as large as memory allows. This differs from languages like Java that have int (32-bit) and long (64-bit).
# Integer literals in various bases
decimal = 255 # base 10 (default)
binary = 0b11111111 # base 2 → 255
octal = 0o377 # base 8 → 255
hexadecimal = 0xFF # base 16 → 255
print(decimal, binary, octal, hexadecimal)
# → 255 255 255 255
# Python supports unbounded integers
factorial_100 = 1
for i in range(1, 101):
factorial_100 *= i
print(factorial_100) # → a 158-digit number, no overflow!
# Division operations — note the differences
print(10 / 3) # → 3.3333... (always a float, even when it divides evenly)
print(10 // 3) # → 3 (floor division, result is int)
print(10 % 3) # → 1 (remainder / modulo)
print(2 ** 10) # → 1024 (exponentiation)
# Thousands separators for readability
population = 270_000_000
budget = 1_500_000_000
print(population) # → 270000000
float — Decimal Numbers
#
float in Python uses the IEEE 754 double precision (64-bit) representation. This gives about 15–17 digits of decimal precision, but it also brings consequences that often surprise beginners.
# Float literals
pi = 3.14159
avogadro = 6.022e23 # scientific notation: 6.022 × 10²³
tiny = 1.5e-10 # 1.5 × 10⁻¹⁰
print(avogadro) # → 6.022e+23
print(tiny) # → 1.5e-10
Float Precision Trap #
# ANTI-PATTERN: comparing floats directly
print(0.1 + 0.2) # → 0.30000000000000004 (not 0.3!)
print(0.1 + 0.2 == 0.3) # → False ← the classic trap!
# Why? Because 0.1 and 0.2 can't be represented
# exactly in binary (like 1/3 in decimal)
# CORRECT: use math.isclose() for float comparison
import math
print(math.isclose(0.1 + 0.2, 0.3)) # → True
print(math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9)) # → True
# Or round() before comparing
print(round(0.1 + 0.2, 10) == round(0.3, 10)) # → True
# ANTI-PATTERN: using float for money/financial calculations
price = 19.99
tax = 0.11
total = price * (1 + tax)
print(total) # → 22.18890000000000... (not precise for money!)
# CORRECT: use the decimal module for financial calculations
from decimal import Decimal, ROUND_HALF_UP
price = Decimal("19.99")
tax = Decimal("0.11")
total = price * (1 + tax)
print(total) # → 22.1889
# Round to 2 decimal places
total_rounded = total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(total_rounded) # → 22.19
Never usefloatfor calculations involving money, finance, or values that need exact decimal precision. Use thedecimalmodule from the Python stdlib — it’s purpose-built for this need.
str — Strings (Text)
#
A string in Python is a sequence of Unicode characters that is immutable — once created, its value can’t be changed. Every string operation produces a new string.
# Ways to create strings
s1 = 'single quotes'
s2 = "double quotes"
s3 = """multi-line
string
with triple-quote"""
s4 = '''this also
works multi-line'''
# Raw strings — backslashes are not interpreted as escapes
windows_path = r"C:\Users\Budi\Documents" # r = raw string
regex_pattern = r"\d+\.\d+" # useful for regex
print(windows_path) # → C:\Users\Budi\Documents
# Bytes strings — for binary data
binary_data = b"hello"
print(type(binary_data)) # → <class 'bytes'>
Important String Operations #
text = "Selamat Datang di Python"
# Indexing and slicing
print(text[0]) # → S (first index)
print(text[-1]) # → n (last index)
print(text[0:7]) # → Selamat
print(text[8:]) # → Datang di Python
print(text[:7]) # → Selamat
print(text[::2]) # → SlmtDtn iPto (every 2 characters)
print(text[::-1]) # → nohtyP id gnataDtamlaleS (reversed)
# Frequently used string methods
print(text.upper()) # → SELAMAT DATANG DI PYTHON
print(text.lower()) # → selamat datang di python
print(text.split(" ")) # → ['Selamat', 'Datang', 'di', 'Python']
print(text.replace("Python", "Dunia")) # → Selamat Datang di Dunia
print(" spasi ".strip()) # → spasi
print(text.startswith("Sel")) # → True
print(text.endswith("on")) # → True
print("Python" in text) # → True
print(len(text)) # → 24
String Formatting #
name = "Budi"
score = 92.5
rank = 3
# f-strings (Python 3.6+) — the recommended way
print(f"Hello, {name}!") # → Hello, Budi!
print(f"Score: {score:.1f}") # → Score: 92.5
print(f"Score: {score:.0f}") # → Score: 93 (rounded)
print(f"Rank: {rank:02d}") # → Rank: 03
print(f"Value: {score!r}") # → Value: 92.5
print(f"{'left':<10}|{'center':^10}|{'right':>10}")
# → left | center | right
# ANTI-PATTERN: concatenation in a loop — very slow for long strings
word_list = ["Python", "is", "a", "great", "language"]
result = ""
for word in word_list:
result += word + " " # creates a new string every iteration
# CORRECT: use join()
result = " ".join(word_list)
print(result) # → Python is a great language
bool — Booleans
#
bool is a subclass of int in Python. True equals 1 and False equals 0 — this enables some concise tricks but is also a source of confusion.
print(True + True) # → 2
print(True * 5) # → 5
print(False + 1) # → 1
print(isinstance(True, int)) # → True (bool is a subclass of int!)
# Truthy and falsy values
# All of the values below are considered False when used in a condition:
falsy_values = [
False, 0, 0.0, 0j, # zero numbers
"", '', b"", # empty strings
[], (), {}, set(), # empty collections
None, # None
]
# Every other value is considered True
print(bool(42)) # → True
print(bool(-1)) # → True (even negatives are True!)
print(bool("")) # → False
print(bool("0")) # → True (the string "0" is not zero!)
print(bool([])) # → False
print(bool([0])) # → True (a list with 1 element, even zero)
# Using truthy/falsy idiomatically
name = ""
# ANTI-PATTERN: explicit comparison with an empty string
if name == "":
print("name is empty")
# CORRECT: just evaluate directly
if not name:
print("name is empty")
# Another example: checking an empty list
data = []
if not data:
print("no data")
None — No Value
#
None is the only value of the NoneType type. It’s used to represent the absence of a value, similar to null in other languages.
# Common uses of None
def find_user(user_id):
# returns None if not found
if user_id not in database:
return None
return database[user_id]
result = find_user(999)
# ANTI-PATTERN: comparing None with ==
if result == None:
print("not found")
# CORRECT: always use 'is' or 'is not' for None
if result is None:
print("not found")
if result is not None:
print(f"found: {result}")
# None as a default parameter (a common Python pattern)
def add_to_list(value, target=None):
# ANTI-PATTERN: using a list as the direct default
# def add_to_list(value, target=[]): ← DANGEROUS! the default list is shared across calls
# CORRECT: use None as a sentinel, create a new list inside the function
if target is None:
target = []
target.append(value)
return target
print(add_to_list(1)) # → [1]
print(add_to_list(2)) # → [2] (a new list, not [1, 2]!)
Collection Types #
list — Ordered, Mutable Collection
#
# Creating lists
numbers = [1, 2, 3, 4, 5]
mixed = [42, "hello", 3.14, True, None] # different types allowed
nested = [[1, 2], [3, 4], [5, 6]] # lists inside lists
empty = []
# Basic operations
numbers.append(6) # add at the end → [1,2,3,4,5,6]
numbers.insert(0, 0) # add at index 0 → [0,1,2,3,4,5,6]
numbers.pop() # remove & return the last element → 6
numbers.pop(0) # remove & return the element at index 0 → 0
numbers.remove(3) # remove the value 3 (first occurrence)
numbers.sort() # sort in place
numbers.reverse() # reverse order in place
print(len(numbers)) # → number of elements
print(3 in numbers) # → True/False
tuple — Ordered, Immutable Collection
#
# Creating tuples
coordinates = (10.5, -6.2)
rgb = (255, 128, 0)
single_element = (42,) # the comma is required for a 1-element tuple!
empty = ()
# TRAP: a 1-element tuple without the comma
not_a_tuple = (42) # this is a plain int, not a tuple
print(type(not_a_tuple)) # → <class 'int'>
print(type((42,))) # → <class 'tuple'>
# Tuples are immutable — can't be changed
coordinates[0] = 99 # TypeError: 'tuple' object does not support item assignment
# When to use tuple vs list?
# Tuple: data that shouldn't change (coordinates, RGB, database records)
# List: data that will be modified (shopping cart, processing queue)
dict — Key-Value Pairs
#
# Creating a dict
user = {
"name": "Budi Santoso",
"age": 28,
"email": "[email protected]",
"active": True,
}
# Accessing values
print(user["name"]) # → Budi Santoso
print(user.get("phone")) # → None (no error if the key doesn't exist)
print(user.get("phone", "-")) # → - (default value)
# ANTI-PATTERN: direct access without checking
phone = user["phone"] # KeyError if the key doesn't exist
# CORRECT: use .get() for keys that may not exist
phone = user.get("phone", "not available")
# Dict operations
user["phone"] = "081234567890" # add/update a key
del user["active"] # delete a key
print("email" in user) # → True (check key existence)
# Iteration
for key in user:
print(key)
for key, value in user.items():
print(f"{key}: {value}")
print(list(user.keys())) # → ['name', 'age', 'email', 'phone']
print(list(user.values())) # → ['Budi Santoso', 28, '[email protected]', '...']
set — Collection of Unique Values
#
# Creating a set
fruits = {"apple", "orange", "mango", "apple"} # duplicates removed automatically
print(fruits) # → {'apple', 'orange', 'mango'} (order not guaranteed)
# An empty set MUST use set(), not {}
empty = set() # ✓ empty set
not_a_set = {} # ✗ this is an empty dict, not a set!
# Set operations
a = {1, 2, 3, 4, 5}
b = {3, 4, 5, 6, 7}
print(a | b) # union → {1,2,3,4,5,6,7}
print(a & b) # intersection → {3,4,5}
print(a - b) # difference → {1,2}
print(a ^ b) # symmetric difference → {1,2,6,7}
# Common use: removing duplicates from a list
data = [1, 2, 2, 3, 3, 3, 4]
unique = list(set(data))
print(unique) # → [1, 2, 3, 4] (order not guaranteed)
# Membership check — O(1), much faster than a list
print(3 in a) # → True
Mutable vs Immutable #
This distinction is crucial to understand — many hidden bugs come from misunderstanding which types are mutable and which aren’t.
Immutable (can't be changed): Mutable (can be changed):
───────────────────────────── ────────────────────────
int, float, complex list
str dict
bool set
tuple bytearray
frozenset
bytes
# Immutable: operations produce a NEW object
s = "hello"
s_new = s.upper() # s doesn't change, s_new is a new object
print(s) # → hello
print(s_new) # → HELLO
# Mutable: operations modify THE SAME object
lst = [1, 2, 3]
lst.append(4) # lst is modified in place
print(lst) # → [1, 2, 3, 4]
# Important consequence: mutable objects can't be dict keys
d = {}
d[(1, 2)] = "tuple as key" # ✓ tuples are immutable
d[[1, 2]] = "list as key" # ✗ TypeError: unhashable type: 'list'
Type Conversion #
Python provides built-in functions for explicit conversion between types. It’s important to remember: not every conversion will succeed — some can raise exceptions.
# int() — convert to integer
print(int("42")) # → 42
print(int(3.99)) # → 3 (truncated, not rounded!)
print(int(True)) # → 1
print(int("0xFF", 16)) # → 255 (hex string to int)
# These will fail:
# int("3.14") → ValueError: invalid literal for int()
# int("hello") → ValueError
# float() — convert to float
print(float("3.14")) # → 3.14
print(float(42)) # → 42.0
print(float("inf")) # → inf
# str() — convert to string (always succeeds)
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() — convert to boolean
print(bool(0)) # → False
print(bool("")) # → False
print(bool([])) # → False
print(bool(42)) # → True
print(bool("false")) # → True (any non-empty string is True!)
# Collection conversion
print(list((1, 2, 3))) # tuple → list: [1, 2, 3]
print(tuple([1, 2, 3])) # list → tuple: (1, 2, 3)
print(set([1, 2, 2, 3])) # list → set: {1, 2, 3}
print(list("hello")) # str → list: ['h','e','l','l','o']
print("".join(['h','i'])) # list → str: "hi"
Safe Conversion with Error Handling #
def to_int_safe(value, default=0):
"""Convert to int without raising an exception."""
try:
return int(value)
except (ValueError, TypeError):
return default
print(to_int_safe("42")) # → 42
print(to_int_safe("abc")) # → 0 (default)
print(to_int_safe("abc", -1)) # → -1 (custom default)
print(to_int_safe(None)) # → 0
Checking Data Types #
x = 42
# type() — returns the exact type
print(type(x)) # → <class 'int'>
print(type(x) == int) # → True
# isinstance() — checks the type including subclasses (more recommended)
print(isinstance(x, int)) # → True
print(isinstance(x, (int, float))) # → True (checks several types at once)
print(isinstance(True, int)) # → True (bool is a subclass of int)
# ANTI-PATTERN: type() for type checks — doesn't recognize subclasses
print(type(True) == int) # → False (even though True is a subclass of int)
# CORRECT: isinstance() for type checks
print(isinstance(True, int)) # → True
Summary #
intis unbounded — no need to worry about overflow like in C/Java. Python manages memory for large integers automatically.- Don’t compare floats with
==— usemath.isclose(). The binary representation of floats isn’t exact for most decimals.- Don’t use
floatfor money calculations — usedecimal.Decimalfor correct financial precision.- Strings are immutable — every string operation produces a new object. Use
"".join(list)instead of concatenation in loops.Noneis always compared withis/is not, not==/!=.- An empty set must be
set(), not{}— empty curly braces are an empty dict.- Mutable objects can’t be dict keys — use tuples (immutable) as keys, not lists.
isinstance()is better thantype()— it recognizes subclasses and is more flexible for type checking.bool("false")evaluates toTrue— any non-empty string is truthy, including the strings"false","0", and"None".