Python 3.12+ · Updated November 2026 · Every program explained line by line

Python Data Types
Every type, every line explained

A complete, runnable reference for Python's built-in data types. Every code block includes a numbered line-by-line breakdown so nothing feels like magic — and a Run button so you can test everything instantly in your browser. Includes three practice exercises and a full quick-reference table.

# A first look at Python's built-in types
age: int = 25
height: float = 1.75
name: str = "Ana"
is_student: bool = True
hobbies: list = ["reading", "coding"]
coordinates: tuple = (40.71, -74.01)
scores: dict = {"math": 95, "science": 88}
unique_tags: set = {"python", "tutorial"}
nothing_here: NoneType = None

for value in (age, height, name, is_student, hobbies, coordinates, scores, unique_tags, nothing_here):
    print(f"{value!r:>24}  →  {type(value).__name__}")
Written & reviewed by Glenn Junsay Pansensoy Last reviewed: November 2026 Python 3.12+ ~30 min read

🧬 What Is a Type, Really?

Every value in Python has a type that determines what it can do

A type is the blueprint for a value. It answers three questions: what data does this value hold, what operations can I perform on it, and how much memory does it need? In Python you almost never declare a type explicitly — the interpreter infers it from the value you assign. But knowing exactly which type you're working with is what separates guessing from debugging.

🔍 Checking Any Value's Type

Use type() to get the exact type object, and isinstance() to test membership in a type hierarchy. The latter is safer when subclasses are involved.

values = [42, 3.14, "hello", True, [1, 2], (1, 2), {"a": 1}, {1, 2}, None]

for v in values:
    print(f"{str(v):<14} type={type(v).__name__:<10} isinstance(int)? {isinstance(v, int)}")
Line-by-line explanation
  1. 1values = [...] — a single list holding one example of each core built-in type: int, float, str, bool, list, tuple, dict, set, and None.
  2. 2for v in values: — iterates over each value, binding the loop variable v to the current item. No index needed; Python's for is a for‑each loop.
  3. 3print(f"...") — an f-string containing three expressions inside {} braces: str(v) renders the value as readable text, type(v).__name__ extracts the type's name as a string (e.g. "int", "list"), and isinstance(v, int) returns True only for integer values.
  4. 4<14 and :<10 inside the f-string are format specifiers — left-align each field in 14 and 10 characters respectively so the columns line up.
Expected output42 type=int isinstance(int)? True 3.14 type=float isinstance(int)? False hello type=str isinstance(int)? False True type=bool isinstance(int)? True [1, 2] type=list isinstance(int)? False (1, 2) type=tuple isinstance(int)? False {'a': 1} type=dict isinstance(int)? False {1, 2} type=set isinstance(int)? False None type=NoneType isinstance(int)? False
💡 Notice that True reports isinstance(int)? True. In Python, bool is a subclass of int — True == 1 and False == 0. That's a deliberate design choice, not a bug.

🔢 Numeric Types — int, float, complex

Whole numbers, decimal numbers, and complex numbers — each with different precision and range

🔢 Integers — Arbitrary Precision

Python integers have no fixed size. They grow as large as your memory allows — no overflow, no wrapping. You can write them in decimal, binary, octal, or hexadecimal.

a = 42
b = 1_000_000
c = 0b101010
d = 0o52
e = 0x2A

print(f"a={a}  b={b}  c={c}  d={d}  e={e}")
print(f"Same value? {a == c == d == e}")

big = 2 ** 1000
print(f"2^1000 has {len(str(big))} digits")
Line-by-line explanation
  1. 1a = 42 — a decimal integer literal. The variable a is bound to the int object representing forty-two.
  2. 2b = 1_000_000 — underscores are visual separators only. The value is exactly one million; Python ignores the underscores.
  3. 3c = 0b101010 — a binary literal. 0b prefix means base 2. 101010₂ = 42₁₀.
  4. 4d = 0o52 — an octal literal. 0o prefix means base 8. 52₈ = 42₁₀.
  5. 5e = 0x2A — a hexadecimal literal. 0x prefix means base 16. 2A₁₆ = 42₁₀.
  6. 6print(...) prints all five values; each prints as 42 because Python normalises numeric literals to their numeric value regardless of how they were written.
  7. 7a == c == d == e — a chained comparison. Python evaluates this as a == c and c == d and d == e, which is True because all four names refer to the same numeric value.
  8. 8big = 2 ** 1000 — exponentiation operator **. Computes 2 to the 1000th power — an enormous integer with over 300 digits. Python handles it without overflow.
  9. 9len(str(big)) converts the huge integer to its string representation, then counts the characters — revealing how many digits it has. The result: 302 digits.
💡 Because int is arbitrary‑precision, 2 ** 1000 is computed exactly. In most other languages, this would overflow a fixed 64‑bit register.

🔣 Floating-Point Numbers — Approximate, Not Exact

A float is a 64‑bit IEEE‑754 double-precision number. It can represent very large and very small numbers — but not every decimal fraction exactly. 0.1 + 0.2 is not exactly 0.3. That's a property of the binary representation, not a Python bug.

price = 19.99
tax_rate = 0.08
total = price * (1 + tax_rate)

print(f"Total: {total}")
print(f"Rounded: {round(total, 2)}")
print(f"0.1 + 0.2 = {0.1 + 0.2}")
print(f"Equal to 0.3? {0.1 + 0.2 == 0.3}")

import math
print(f"Approx equal? {math.isclose(0.1 + 0.2, 0.3)}")

from decimal import Decimal
exact_total = Decimal("19.99") * (Decimal(1) + Decimal("0.08"))
print(f"Exact with Decimal: {exact_total}")
Line-by-line explanation
  1. 1price = 19.99 — a float literal. Stored in binary as the closest representable double to 19.99.
  2. 2tax_rate = 0.08 — another float. Like all decimal fractions, 0.08 cannot be represented exactly in binary.
  3. 3total = price * (1 + tax_rate) — multiplication and addition. The result carries tiny floating-point error accumulated through both operations.
  4. 4The first print shows the raw float — likely 21.5892 with a long tail of unexpected digits.
  5. 5round(total, 2) rounds to two decimal places. Useful for display, but don't rely on rounding to fix all floating-point issues in financial calculations.
  6. 60.1 + 0.2 yields 0.30000000000000004 — the classic demonstration. Neither 0.1 nor 0.2 is exactly representable in binary.
  7. 70.1 + 0.2 == 0.3 returns False because the two values differ by about 5.5e‑17 — small, but not zero.
  8. 8math.isclose() is the correct way to compare floats. It checks whether the two numbers are within a small relative tolerance — default 1e‑09.
  9. 9Decimal("19.99") — the decimal module provides exact decimal arithmetic. Note the strings: Decimal("19.99") is exact, while Decimal(19.99) would inherit the float's imprecision.
💡 For money, use Decimal or store amounts as integer cents. Never use float for financial calculations you care about.

🌀 Complex Numbers — Real + Imaginary

Python has built-in support for complex numbers using j as the imaginary unit. Used in signal processing, electrical engineering, and some mathematical computations.

z = 3 + 4j

print(f"z = {z}")
print(f"Real part: {z.real}")
print(f"Imaginary part: {z.imag}")
print(f"Magnitude: {abs(z)}")
print(f"Conjugate: {z.conjugate()}")
Line-by-line explanation
  1. 1z = 3 + 4j — a complex number literal. j is the imaginary unit (√‑1). Python writes it as (3+4j).
  2. 2z.real — the real component, 3.0 as a float.
  3. 3z.imag — the imaginary component, 4.0.
  4. 4abs(z) — the magnitude (modulus) of the complex number: √(3² + 4²) = 5.0.
  5. 5z.conjugate() — the complex conjugate: 3 - 4j.

📝 Text — the str Type

Immutable sequences of Unicode characters — Python's most flexible and most used type

✍️ Creating and Inspecting Strings

Strings are created with single quotes, double quotes, or triple quotes (for multi-line). They are immutable — every method returns a new string rather than modifying the original.

name = "Ana"
greeting = 'Hello'
paragraph = """Line one
Line two"""

print(f"Length: {len(name)}")
print(f"First char: {name[0]}")
print(f"Last char: {name[-1]}")
print(f"Reversed: {name[::-1]}")
print(f"Uppercase: {name.upper()}")
print(f"Original unchanged: {name}")
print(f"Split: {'Hello World'.split()}")
print(f"Join: {', '.join(['a', 'b', 'c'])}")
Line-by-line explanation
  1. 1name = "Ana" — a string literal. The str object is created and bound to name.
  2. 2greeting = 'Hello' — single quotes work exactly the same as double quotes. The choice is style, not substance.
  3. 3paragraph = """Line one\nLine two""" — triple-quoted string spans multiple lines. The newline is included in the string.
  4. 4len(name) returns 3 — the number of characters in the string.
  5. 5name[0] — indexing. Python uses zero‑based indexing, so the first character is at index 0: 'A'.
  6. 6name[-1] — negative indexing counts from the end. The last character is at index ‑1: 'a'.
  7. 7name[::-1] — slicing with step ‑1 reverses the string: 'anA'.
  8. 8name.upper() returns 'ANA' — a new string. The original name is untouched.
  9. 9The line Original unchanged: Ana confirms that string methods never mutate in place.
  10. 10"Hello World".split() returns ['Hello', 'World'] — splitting on whitespace by default.
  11. 11", ".join(...) joins a list of strings into one string with ", " between each element.
💡 Because strings are immutable, s[0] = "B" raises TypeError. To "change" a string, you must create a new one — like s = "B" + s[1:].

🎨 f-strings — The Modern Way to Format

Introduced in Python 3.6, f-strings are the recommended way to embed values inside strings. They are evaluated at runtime, support any expression, and offer rich format specifications.

name = "Ana"
score = 95.678
items = 1234567

print(f"Hello, {name}!")
print(f"Score: {score:.1f}")
print(f"Score: {score:.2f}")
print(f"Padded: {name:>8}|")
print(f"Centred: {name:^8}|")
print(f"Percent: {0.856:.1%}")
print(f"Comma: {items:,}")
print(f"Debug: {name=} {score=}")
Line-by-line explanation
  1. 1f"Hello, {name}!" — the f prefix marks an f-string. Anything inside {} is evaluated as a Python expression and inserted.
  2. 2{score:.1f} — format spec .1f means "fixed-point, one decimal place". Result: 95.7.
  3. 3{score:.2f} — two decimal places: 95.68.
  4. 4{name:>8} — right-aligns within 8 characters. With "Ana", result is " Ana".
  5. 5{name:^8} — centres within 8 characters: " Ana ".
  6. 6{0.856:.1%} — percentage format: 85.6%.
  7. 7{items:,} — comma as thousands separator: 1,234,567.
  8. 8{name=} — the = debug syntax (Python 3.8+) prints both the expression and its value: name='Ana' score=95.678.

✅ Boolean — True and False

The smallest type, but behind every comparison, condition, and logical expression

🧠 Truthiness — What Counts as True?

Every Python value can be tested in a boolean context. Rather than memorising rules, think about which values are empty or zero — those are falsy. Everything else is truthy.

falsy_values = [False, None, 0, 0.0, "", [], (), {}, set()]

for v in falsy_values:
    print(f"{v!r:<10} → bool is {bool(v)}")

truthy_values = [True, 1, -1, 3.14, "0", [0], (0,), {"key": None}]

for v in truthy_values:
    print(f"{v!r:<10} → bool is {bool(v)}")
Line-by-line explanation
  1. 1A list of values that Python considers falsy — False, None, zero of any numeric type, and any empty container.
  2. 2–3Loops over each falsy candidate and prints its bool() conversion. All print False.
  3. 5A list of values that Python considers truthy — even "0" (the non‑empty string) and [0] (a list containing zero).
  4. 7–8Loops and prints — all print True. A container is truthy if it contains anything, regardless of what that thing is.
💡 Don't write if len(my_list) > 0: — write if my_list:. Python's truthiness rules let you test for "non-empty" directly. It's idiomatic, readable, and works for every container type.

⚡ Short-Circuit Evaluation

The operators and and or stop evaluating as soon as the outcome is determined. This is not just an optimisation — it's often a deliberate guard against errors.

a = 0
b = 5

print(a and b)
print(a or b)
print(not a)

name = ""
display = name or "Anonymous"
print(display)

safe = b != 0 and (100 / b)
print(safe)
Line-by-line explanation
  1. 1–2Two integers: a = 0 (falsy) and b = 5 (truthy).
  2. 4a and b — since a is falsy, and short‑circuits and returns a itself: 0.
  3. 5a or b — since a is falsy, or returns b: 5.
  4. 6not a — a is 0, so not a is True.
  5. 8–9name or "Anonymous" — a standard Python idiom for supplying a default when a value is falsy. Since name is "", the result is "Anonymous".
  6. 11–12b != 0 and (100 / b) — if b were zero, and would short‑circuit and the division would never run. This pattern prevents ZeroDivisionError.

📦 Collection Types — list, tuple, dict, set

Four data structures for organising many values — each with different trade‑offs

📋 list — Ordered, Mutable, Allows Duplicates

fruits = ["apple", "banana", "cherry"]

fruits.append("date")
fruits.insert(1, "blueberry")
fruits.remove("banana")
popped = fruits.pop()

print(f"Final list: {fruits}")
print(f"Popped: {popped}")
print(f"First two: {fruits[:2]}")
print(f"Squares: {[x**2 for x in range(1, 6)]}")
Line-by-line explanation
  1. 1A list literal with three strings. Lists preserve insertion order and allow duplicate values.
  2. 3.append("date") adds one element to the end — O(1) amortised.
  3. 4.insert(1, "blueberry") inserts at index 1, shifting everything right — O(n).
  4. 5.remove("banana") removes the first occurrence of that value.
  5. 6.pop() removes and returns the last element — "date".
  6. 8–9Prints the final list and the popped value.
  7. 10fruits[:2] — slicing from start to (but not including) index 2.
  8. 11A list comprehension: [x**2 for x in range(1, 6)] creates [1, 4, 9, 16, 25].

🎯 tuple — Ordered, Immutable

point = (3, 7)
rgb = (255, 128, 0)

x, y = point
print(f"x={x}, y={y}")

first, *middle, last = rgb
print(f"first={first} middle={middle} last={last}")

single = (42,)
print(f"Type: {type(single).__name__}, length: {len(single)}")
Line-by-line explanation
  1. 1point = (3, 7) — a tuple literal. Once created, its elements cannot be changed.
  2. 2rgb = (255, 128, 0) — a three‑element tuple. Order is significant.
  3. 4x, y = point — tuple unpacking. Values are assigned left‑to‑right from the tuple.
  4. 7first, *middle, last = rgb — starred unpacking: first = 255, middle = [128], last = 0.
  5. 10single = (42,) — the trailing comma is crucial. Without it, (42) is just the integer 42, not a tuple.
💡 Tuples are faster and lighter than lists. Use them for fixed collections like coordinates, RGB values, or database records — and when you need a hashable sequence for a dictionary key.

📖 dict — Key-Value Pairs, O(1) Lookup

student = {"name": "Ana", "age": 25, "scores": [95, 88]}

student["grade"] = "A"
print(student.get("email", "not provided"))

for key, value in student.items():
    print(f"{key}: {value}")

keys = list(student.keys())
values = list(student.values())
print(f"Keys: {keys}")
print(f"Values: {values}")
Line-by-line explanation
  1. 1A dict literal with three key‑value pairs. Keys must be hashable — strings, numbers, or tuples.
  2. 3student["grade"] = "A" — assigns a new key‑value pair. If the key existed, it would be overwritten.
  3. 4.get("email", "not provided") — returns "not provided" instead of raising KeyError. The second argument is the default.
  4. 6–7.items() returns (key, value) tuples, unpacked into key and value for each iteration.
  5. 9–10.keys() and .values() return view objects; list() materialises them into lists.
💡 Never write if "key" in d: v = d["key"] when you can write v = d.get("key", default). The second version is shorter, safer, and does a single lookup.

🔵 set — Unordered, Unique

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(f"Union: {a | b}")
print(f"Intersection: {a & b}")
print(f"Difference: {a - b}")
print(f"Symmetric difference: {a ^ b}")
print(f"Is 3 in a? {3 in a}")
print(f"Deduplicated: {set([1, 2, 2, 3, 3, 3])}")
Line-by-line explanation
  1. 1–2Two sets. Order is not guaranteed and duplicates are automatically removed.
  2. 4a | b — union: all elements from either set. Result: {1, 2, 3, 4, 5, 6}.
  3. 5a & b — intersection: elements present in both. Result: {3, 4}.
  4. 6a - b — difference: elements in a but not b. Result: {1, 2}.
  5. 7a ^ b — symmetric difference: elements in either set but not both. Result: {1, 2, 5, 6}.
  6. 83 in a — membership test. O(1) time regardless of set size.
  7. 9set([1, 2, 2, 3, 3, 3]) removes duplicates. The result is {1, 2, 3}.

🚫 None — The Absence of a Value

NoneType has exactly one value: None. It is not zero, not an empty string, not False — it's the absence of a value.

🚫 None and is None

result = None

def find_user(user_id):
    users = {1: "Ana", 2: "Bob"}
    return users.get(user_id)

found = find_user(3)
print(f"Found: {found}")
print(f"Is it None? {found is None}")
print(f"Type: {type(None).__name__}")
print(f"Bool of None: {bool(None)}")
Line-by-line explanation
  1. 1result = None — None is a keyword-like literal. NoneType is a singleton: only one None object exists.
  2. 3–5find_user returns users.get(user_id). Since key 3 doesn't exist, .get() returns None.
  3. 7found receives None.
  4. 8found is None — the recommended way to compare to None. Use is, not ==; it's faster and not overridable.
  5. 9type(None).__name__ reports "NoneType".
  6. 10bool(None) is False — None is falsy in conditional contexts.
💡 Use x is None, never x == None. Use if x is not None: to check for existence — not if x:, which is also False for 0, "", and empty containers.

🔁 Type Conversion — Casting Between Types

Explicit conversions are almost always safer than implicit ones

🔁 Explicit Casting with int(), float(), str(), list()

print(int("42"))
print(int(3.99))
print(float(7))
print(str(100))
print(list("abc"))
print(tuple([1, 2, 3]))
print(set([1, 2, 2, 3]))

try:
    int("hello")
except ValueError as e:
    print(f"Cannot convert: {e}")
Line-by-line explanation
  1. 1int("42") parses the string into the integer 42. Leading/trailing whitespace is ignored.
  2. 2int(3.99) truncates the decimal portion — result is 3, not 4. It does not round.
  3. 3float(7) converts the integer to 7.0.
  4. 4str(100) produces "100".
  5. 5list("abc") iterates the string, producing ['a', 'b', 'c'].
  6. 6tuple([1, 2, 3]) converts a list to a tuple.
  7. 7set(...) removes duplicates — result is {1, 2, 3}.
  8. 9–11int("hello") raises ValueError because the string is not a valid integer literal. Always wrap user input in try/except.

🔒 Mutable vs Immutable — A Distinction That Matters

Some types can be changed in place; others cannot. This difference drives real bugs when misunderstood

⚡ The Assignment Trap

Assignment in Python binds a name to an object. It does not copy the object. For mutable objects like lists, two names can point to the same data.

# Immutable — reassignment creates a new object
a = 10
b = a
a = 20
print(f"a={a}, b={b}")

# Mutable — alias shares the same object
list_a = [1, 2, 3]
list_b = list_a
list_a.append(4)
print(f"list_a={list_a}, list_b={list_b}")

# Safe copy — independent
list_c = list_a.copy()
list_a.append(99)
print(f"list_a={list_a}, list_c={list_c}")
Line-by-line explanation
  1. 2–5a = 10; b = a — b points to the same integer object. Then a = 20 rebinds a to a new object; b still points to 10. Integers are immutable, so this is safe.
  2. 8–11list_b = list_a — both names point to the same list. list_a.append(4) modifies the shared object, so list_b sees the change too.
  3. 14–16list_c = list_a.copy() creates a new independent list. Appending to list_a has no effect on list_c.
💡 The same trap applies to dictionaries and sets — they are mutable. When you pass a list to a function, the function receives a reference to the original object unless you copy it.

Immutable Types

int, float, complex, str, bool, tuple, frozenset, NoneType, bytes — safe to share, usable as dict keys.

Mutable Types

list, dict, set, bytearray, most custom class instances — changes propagate through every reference.

🏋️ Practice — Check Your Understanding

Three exercises with solutions. Try each before expanding the answer.

Exercise 1 — Identify the Types

What are the types of each value? Predict before running.

values = [42, 3.0, "42", True, None, [42], (42,), {42}, {"answer": 42}]

for v in values:
    print(f"{v!r:<16} → {type(v).__name__}")
Show solution

int, float, str, bool, NoneType, list, tuple, set, dict — in that order.

Exercise 2 — Fix the Bug

Why does this loop skip the "banana"? Fix it without using a comprehension.

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    if fruit.startswith("b"):
        fruits.remove(fruit)
print(fruits)
Show solution

Removing from a list while iterating shifts indices, so the next element is skipped. Iterate over a copy: for fruit in fruits[:]: — or build a new list. The slice fruits[:] creates a shallow copy so the loop and the mutation don't interfere.

Exercise 3 — Safe Type Conversion

Write a function safe_int(value, default=0) that returns an integer or a default if conversion fails. Test it with "42", "hello", and 3.99.

Show solution
def safe_int(value, default=0):
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

print(safe_int("42"))
print(safe_int("hello", -1))
print(safe_int(3.99))

Catching both ValueError (bad string) and TypeError (unsupported type) makes the helper genuinely safe.

❓ Frequently Asked Questions

Common questions about Python's data types, answered directly

Because float values are stored in binary (IEEE 754 double precision), and the decimal fractions 0.1 and 0.2 have no exact binary representation — just as 1/3 has no exact decimal representation. The sum carries a tiny error (~5.5e‑17). For comparisons, use math.isclose(a, b); for money, use Decimal. This is a property of floating-point arithmetic, not a Python bug.

Use a tuple when the collection is fixed and should not change — coordinates, RGB values, database records, or anything you need to use as a dictionary key (tuples are hashable; lists are not). Use a list when you need to add, remove, or reorder items. Tuples are also slightly faster and lighter in memory.

Yes. True and False are instances of bool, and bool inherits from int. Numerically, True == 1 and False == 0. This is why isinstance(True, int) returns True, and why you can use booleans in arithmetic (though doing so is rarely good style).

Use if x is None: — not if x == None: and not if not x:. The is operator checks identity against the single None object and cannot be overridden by a custom __eq__. Writing if not x: also catches 0, "", [], and False, which is usually not what you mean when checking for None.

📋 Quick-Reference Table

Every built-in type at a glance — bookmark this page for quick revision

Type Example Mutable? Ordered? Duplicates?
int42NoN/AN/A
float3.14NoN/AN/A
str"hello"NoYesYes
boolTrueNoN/AN/A
list[1, 2, 3]YesYesYes
tuple(1, 2, 3)NoYesYes
dict{"a": 1}YesYesNo (keys)
set{1, 2, 3}YesNoNo
NoneTypeNoneNoN/AN/A
Back to curriculum home

Educational Use Disclaimer

This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.

🗣️ Learner Voices

No comments yet — your voice starts the conversation.