Python – Data Types

Variables

In Python, every piece of data lives in a variable — a named container that holds a value. Unlike statically typed languages such as Java or C++, Python is dynamically typed. You never declare a type explicitly; Python infers it the moment you assign a value. A variable can even change its type mid-program. Under the hood, every value is an object (an instance of a class), and every data type is actually a class.

# Python is dynamically typed — no type declarations needed
name = "Folau"
print(type(name))   # 

name = 42
print(type(name))   #   — same variable, different type

name = [1, 2, 3]
print(type(name))   #  — changed again, no error

This flexibility is powerful, but it means you must be deliberate about what your variables hold. Use descriptive names and lean on type() when debugging.

 

Data Types Overview

Python groups its built-in types into several categories. Here is the full reference table:

Text Type str
Numeric Types: intfloatcomplex
Sequence Types: listtuplerange
Mapping Type: dict
Set Types: setfrozenset
Boolean Type: bool
Binary Types: bytesbytearraymemoryview

Let’s walk through each one in detail.

 

str — Text Type

Strings are sequences of Unicode characters. You can create them with single quotes, double quotes, or triple quotes (for multi-line text). Python has no separate char type — a single character is simply a string of length one.

When to use: Any time you work with text — user input, file paths, API responses, log messages.

# Creating strings
first_name = "Folau"
last_name = 'Kaveinga'
bio = """Software developer
who loves Python."""

print(type(first_name))  # 
# String slicing — extract parts of a string
language = "Python"
print(language[0])      # P         — first character
print(language[-1])     # n         — last character
print(language[0:3])    # Pyt       — index 0 up to (not including) 3
print(language[::2])    # Pto       — every other character
print(language[::-1])   # nohtyP    — reversed
# f-strings (Python 3.6+) — the modern way to format strings
name = "Folau"
age = 30
print(f"My name is {name} and I am {age} years old.")
# My name is Folau and I am 30 years old.

price = 49.99
print(f"Total: ${price:.2f}")  # Total: $49.99
# Common string methods
message = "  Hello, Python World!  "
print(message.strip())            # "Hello, Python World!"  — remove whitespace
print(message.strip().lower())    # "hello, python world!"
print(message.strip().upper())    # "HELLO, PYTHON WORLD!"
print(message.strip().split(",")) # ['Hello', ' Python World!']
print(message.strip().replace("Python", "Java"))  # "Hello, Java World!"
print("Python" in message)        # True — membership check
print(message.strip().startswith("Hello"))  # True
print(message.strip().count("l"))           # 3

Common pitfall: Strings are immutable. Every operation that looks like it modifies a string actually creates a new one. If you need to build a string through many concatenations inside a loop, use "".join() or a list — it is significantly faster than repeated +=.

 

int — Integer Type

Integers represent whole numbers with no decimal point. In Python 3, integers have arbitrary precision — they can be as large as your machine’s memory allows. No overflow errors, ever.

When to use: Counters, indices, loop variables, IDs, quantities — anywhere you need exact whole numbers.

# Basic integer operations
count = 10
negative = -42
big_number = 1_000_000  # underscores improve readability (Python 3.6+)

print(type(count))  # 
# Arithmetic with integers
a = 17
b = 5

print(a + b)    # 22   — addition
print(a - b)    # 12   — subtraction
print(a * b)    # 85   — multiplication
print(a / b)    # 3.4  — true division (returns float!)
print(a // b)   # 3    — floor division (returns int)
print(a % b)    # 2    — modulus (remainder)
print(a ** b)   # 1419857  — exponentiation
# Arbitrary precision — Python handles huge numbers natively
huge = 2 ** 100
print(huge)  # 1267650600228229401496703205376
print(type(huge))  #  — still an int, no overflow

Common pitfall: The / operator always returns a float, even if both operands are integers and the result is a whole number. Use // when you specifically need integer division.

 

float — Floating-Point Type

Floats represent decimal numbers. They follow the IEEE 754 double-precision standard and are accurate to about 15–17 significant digits. 1 is an integer; 1.0 is a float.

When to use: Measurements, scientific calculations, percentages, currency calculations (though for money, consider decimal.Decimal for exactness).

# Creating floats
temperature = 98.6
pi = 3.14159
scientific = 1.5e3  # 1500.0 — scientific notation

num = float(2.5)
print(num)        # 2.5
print(type(num))  # 
# Float arithmetic
price = 19.99
tax_rate = 0.08
total = price * (1 + tax_rate)
print(f"Total: ${total:.2f}")  # Total: $21.59

# Rounding
print(round(3.14159, 2))  # 3.14
print(round(2.5))         # 2 — banker's rounding (rounds to even)
# Float precision issue — this is critical to understand
print(0.1 + 0.2)           # 0.30000000000000004  — NOT 0.3!
print(0.1 + 0.2 == 0.3)    # False

# How to compare floats safely
import math
print(math.isclose(0.1 + 0.2, 0.3))  # True

# For financial calculations, use Decimal
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2"))  # 0.3  — exact

Common pitfall: Floating-point arithmetic is not exact. Never compare floats with ==. Use math.isclose() or a tolerance threshold. For money, use decimal.Decimal.

 

complex — Complex Number Type

Complex numbers have a real and imaginary part, written as x + yj where x is the real part and y is the imaginary part.

When to use: Scientific computing, electrical engineering, signal processing, or any domain that works with imaginary numbers.

# Creating complex numbers
z = 3 + 4j
print(type(z))    # 
print(z.real)     # 3.0
print(z.imag)     # 4.0
print(abs(z))     # 5.0  — magnitude (distance from origin)

# Arithmetic
z1 = 2 + 3j
z2 = 1 - 1j
print(z1 + z2)    # (3+2j)
print(z1 * z2)    # (5+1j)

Common pitfall: Python uses j for the imaginary unit, not i (which is the convention in mathematics). This trips up people coming from a math background.

 

list — Ordered Mutable Sequence

A list is an ordered, mutable collection that can hold items of any type. It is the most versatile data structure in Python and the one you will use most often.

When to use: Whenever you need an ordered collection that may change — storing records, building results, stacks, queues.

# Creating lists
fruits = ["apple", "banana", "cherry"]
mixed = [1, 2.5, "hello", True, None]  # mixed types are fine
empty = []
from_constructor = list((1, 2, 3))

print(type(fruits))  # 
# Accessing and slicing
colors = ["red", "green", "blue", "yellow", "purple"]
print(colors[0])       # red       — first element
print(colors[-1])      # purple    — last element
print(colors[1:3])     # ['green', 'blue'] — slice from index 1 to 3
print(colors[::-1])    # ['purple', 'yellow', 'blue', 'green', 'red'] — reversed
# Modifying lists
tasks = ["write code", "test code"]
tasks.append("deploy")          # add to end
tasks.insert(1, "review code")  # insert at index 1
print(tasks)  # ['write code', 'review code', 'test code', 'deploy']

tasks.remove("test code")       # remove by value
popped = tasks.pop()            # remove and return last item
print(popped)  # deploy
print(tasks)   # ['write code', 'review code']
# Iterating and common patterns
numbers = [3, 1, 4, 1, 5, 9, 2, 6]

# Sort (in-place)
numbers.sort()
print(numbers)  # [1, 1, 2, 3, 4, 5, 6, 9]

# List comprehension — the Pythonic way to transform lists
squares = [x ** 2 for x in range(1, 6)]
print(squares)  # [1, 4, 9, 16, 25]

evens = [x for x in range(10) if x % 2 == 0]
print(evens)    # [0, 2, 4, 6, 8]

# Useful built-in functions
print(len(numbers))  # 8
print(sum(numbers))  # 31
print(min(numbers))  # 1
print(max(numbers))  # 9

Common pitfall: Lists are mutable and passed by reference. If you assign b = a, both variables point to the same list. Modifying one modifies the other. Use b = a.copy() or b = a[:] to create a true copy.

# The mutable reference trap
a = [1, 2, 3]
b = a           # b points to the SAME list
b.append(4)
print(a)        # [1, 2, 3, 4] — a changed too!

# The fix
a = [1, 2, 3]
b = a.copy()    # b is an independent copy
b.append(4)
print(a)        # [1, 2, 3] — a is unchanged

 

tuple — Ordered Immutable Sequence

A tuple is just like a list, except it cannot be modified after creation. Tuples are slightly faster and use less memory than lists.

When to use: Fixed collections of values (coordinates, RGB colors, database rows), dictionary keys (lists cannot be keys), returning multiple values from a function.

# Creating tuples
point = (10, 20)
rgb = (255, 128, 0)
single = (42,)       # trailing comma is required for single-element tuples
from_constructor = tuple([1, 2, 3])

print(type(point))   # 
# Accessing tuple elements
nested_tuple = (1, 2, (3, 4), (5, 6, 7))
print(nested_tuple[0])      # 1
print(nested_tuple[2][0])   # 3
print(nested_tuple[-1])     # (5, 6, 7)

# Tuple unpacking — a very common Python pattern
coordinates = (40.7128, -74.0060)
lat, lng = coordinates
print(f"Latitude: {lat}, Longitude: {lng}")
# Latitude: 40.7128, Longitude: -74.006
# Tuples as function return values
def get_user():
    return "Folau", 30, "developer"

name, age, role = get_user()
print(f"{name} is a {age}-year-old {role}")

Common pitfall: A single value in parentheses like (42) is NOT a tuple — it is just the integer 42 in parentheses. You need the trailing comma: (42,).

 

range — Immutable Sequence of Numbers

The range() function generates a lazy sequence of integers. It does not store all values in memory — it computes them on the fly, making it very memory efficient.

When to use: for loops, generating index sequences, any time you need a predictable series of integers.

# range(start, stop, step)
# start — where to begin (inclusive, defaults to 0)
# stop  — where to end (exclusive)
# step  — increment size (defaults to 1)

# Basic usage
for i in range(5):
    print(i, end=" ")  # 0 1 2 3 4

print()  # newline

# Even numbers from 2 to 18
even_numbers = range(2, 20, 2)
for num in even_numbers:
    print(num, end=" ")  # 2 4 6 8 10 12 14 16 18

print()

# Counting backwards
for i in range(5, 0, -1):
    print(i, end=" ")  # 5 4 3 2 1
# Convert range to list when you need actual list operations
numbers = list(range(1, 11))
print(numbers)  # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Memory efficiency — range stores only start, stop, step
import sys
list_million = list(range(1_000_000))
range_million = range(1_000_000)
print(sys.getsizeof(list_million))  # ~8 MB
print(sys.getsizeof(range_million)) # 48 bytes!

 

dict — Mapping Type (Key-Value Pairs)

A dictionary stores data as key-value pairs. Keys must be immutable and unique; values can be anything. Dictionaries are insertion-ordered as of Python 3.7+ and are optimized for fast lookups by key.

When to use: Anytime you need to associate keys with values — configuration settings, JSON-like data, caching, counting occurrences, grouping items.

# Creating dictionaries
person = {"first_name": "Folau", "last_name": "Kaveinga", "age": 30}
from_constructor = dict(first_name="Folau", last_name="Kaveinga")
empty = {}

print(type(person))  # 
# Accessing and modifying values
person = {"first_name": "Folau", "last_name": "Kaveinga", "age": 30}

# Access
print(person["first_name"])      # Folau
print(person.get("email", "N/A"))  # N/A — .get() with a default avoids KeyError

# Modify
person["age"] = 31               # update existing key
person["email"] = "folau@dev.com" # add new key-value pair

# Remove
del person["email"]
popped_age = person.pop("age")   # remove and return value
print(popped_age)  # 31
# Iterating over dictionaries
scores = {"Alice": 92, "Bob": 87, "Charlie": 95}

# Loop through keys
for name in scores:
    print(name)

# Loop through values
for score in scores.values():
    print(score)

# Loop through both (most common)
for name, score in scores.items():
    print(f"{name}: {score}")

# Dictionary comprehension
squared = {x: x**2 for x in range(1, 6)}
print(squared)  # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Common dict methods
config = {"host": "localhost", "port": 8080}

print(config.keys())    # dict_keys(['host', 'port'])
print(config.values())  # dict_values(['localhost', 8080])
print("host" in config) # True — check if key exists

# Merge two dicts (Python 3.9+)
defaults = {"host": "0.0.0.0", "port": 80, "debug": False}
overrides = {"port": 8080, "debug": True}
final = defaults | overrides
print(final)  # {'host': '0.0.0.0', 'port': 8080, 'debug': True}

Common pitfall: Accessing a missing key with dict["key"] raises a KeyError. Always use .get(key, default) when the key might not exist, or use collections.defaultdict for automatic defaults.

 

set — Unordered Collection of Unique Items

A set is a mutable, unordered collection that automatically removes duplicates. Sets support mathematical operations like union, intersection, and difference.

When to use: Removing duplicates from a list, membership testing (faster than lists), set operations (finding common elements, differences).

# Creating sets
numbers = {1, 2, 3, 4, 5}
from_list = set([1, 2, 2, 3, 3, 3])  # duplicates removed
print(from_list)       # {1, 2, 3}
print(type(numbers))   # 
# Set operations
frontend = {"HTML", "CSS", "JavaScript", "React"}
backend = {"Python", "Java", "JavaScript", "SQL"}

print(frontend & backend)   # {'JavaScript'}         — intersection
print(frontend | backend)   # all combined            — union
print(frontend - backend)   # {'HTML', 'CSS', 'React'} — difference
print(frontend ^ backend)   # symmetric difference (in one but not both)
# Practical use: removing duplicates
names = ["Alice", "Bob", "Alice", "Charlie", "Bob"]
unique_names = list(set(names))
print(unique_names)  # ['Alice', 'Bob', 'Charlie'] (order may vary)

Common pitfall: You cannot create an empty set with {} — that creates an empty dict. Use set() for an empty set.

 

frozenset — Immutable Set

A frozenset is an immutable version of a set. Once created, you cannot add or remove elements.

When to use: When you need a set that can serve as a dictionary key or as an element inside another set (regular sets cannot do this because they are mutable).

# Creating a frozenset
vowels = frozenset(['a', 'e', 'i', 'o', 'u'])
print(vowels)          # frozenset({'a', 'e', 'i', 'o', 'u'})
print(type(vowels))    # 

# frozensets support the same read operations as sets
consonants = frozenset(['b', 'c', 'd', 'f'])
print(vowels & consonants)   # frozenset() — no overlap
print(vowels | consonants)   # combined

# But you cannot modify them
# vowels.add('y')  # AttributeError: 'frozenset' object has no attribute 'add'

 

bool — Boolean Type

Booleans represent one of two values: True or False. They are a subclass of int (where True == 1 and False == 0). Python also evaluates non-boolean values in boolean context — this is called “truthiness.”

When to use: Conditional logic, flags, filtering, control flow.

# Boolean basics
is_active = True
is_deleted = False
print(type(is_active))  # 

# Comparison operators return booleans
print(10 > 5)      # True
print(10 == 5)     # False
print("a" in "abc") # True
# Truthy and falsy values
# These are all falsy:
print(bool(0))        # False
print(bool(0.0))      # False
print(bool(""))       # False — empty string
print(bool([]))       # False — empty list
print(bool({}))       # False — empty dict
print(bool(None))     # False

# Everything else is truthy:
print(bool(1))        # True
print(bool(-1))       # True
print(bool("hello"))  # True
print(bool([1, 2]))   # True

Common pitfall: True and False must be capitalized. Writing true or false (lowercase) will raise a NameError. Also, since bool is a subclass of int, True + True == 2 — which can lead to surprises in arithmetic.

 

bytes — Immutable Binary Sequence

The bytes type holds an immutable sequence of integers in the range 0–255. It is used for binary data — file I/O, network protocols, encoding/decoding text.

When to use: Reading binary files, network communication, encoding strings for transmission.

# Creating bytes from a string
text = "My name is Folau"
encoded = bytes(text, 'utf-8')
print(encoded)        # b'My name is Folau'
print(type(encoded))  # 

# Shorthand with .encode()
encoded2 = "Hello".encode('utf-8')
print(encoded2)       # b'Hello'

# Decoding back to string
decoded = encoded.decode('utf-8')
print(decoded)        # My name is Folau

Common pitfall: Bytes are immutable. You cannot change individual elements. Use bytearray when you need to modify binary data in place.

 

bytearray — Mutable Binary Sequence

A bytearray is the mutable counterpart of bytes. You can modify individual bytes after creation.

When to use: When you need to manipulate binary data in place, such as building binary protocols or editing binary files.

# Creating a bytearray
text = "My name is Folau"
arr = bytearray(text, 'utf-8')
print(arr)           # bytearray(b'My name is Folau')
print(type(arr))     # 

# Unlike bytes, bytearray is mutable
arr[0] = 72          # change 'M' (77) to 'H' (72)
print(arr.decode())  # Hy name is Folau

 

memoryview — Buffer Protocol Access

A memoryview lets you access the internal data of a bytes or bytearray object without copying it. This is useful for performance when working with large binary data.

When to use: High-performance binary data manipulation, avoiding unnecessary copies of large buffers.

# Creating a memoryview
data = bytearray('ABC', 'utf-8')
mv = memoryview(data)

print(mv[0])          # 65  — ASCII value of 'A'
print(bytes(mv[0:2])) # b'AB'
print(list(mv[0:3]))  # [65, 66, 67]

# Modify original data through the memoryview (zero-copy)
mv[0] = 90            # change 'A' to 'Z'
print(data)           # bytearray(b'ZBC')

 


 

Type Conversion (Casting)

Python provides built-in functions to convert between types. This is called casting.

# int conversions
print(int(3.9))       # 3      — truncates, does NOT round
print(int("42"))      # 42     — string to int
print(int(True))      # 1      — bool to int
# print(int("3.5"))   # ValueError — cannot convert float string directly

# float conversions
print(float(10))      # 10.0   — int to float
print(float("3.14"))  # 3.14   — string to float
print(float(False))   # 0.0    — bool to float

# string conversions
print(str(42))        # "42"
print(str(3.14))      # "3.14"
print(str(True))      # "True"

# list / tuple conversions
print(list((1, 2, 3)))     # [1, 2, 3]   — tuple to list
print(tuple([1, 2, 3]))    # (1, 2, 3)   — list to tuple
print(list("Python"))      # ['P', 'y', 't', 'h', 'o', 'n']
print(set([1, 2, 2, 3]))   # {1, 2, 3}   — list to set (removes dupes)
print(list({1, 2, 3}))     # [1, 2, 3]   — set to list

Common pitfall: int("3.5") raises a ValueError. You need to convert to float first, then to int: int(float("3.5")).

 

Type Checking

Python provides two main ways to check the type of a variable:

# type() — returns the exact type
name = "Folau"
age = 30
scores = [90, 85, 92]

print(type(name))    # 
print(type(age))     # 
print(type(scores))  # 

# Comparing types directly
if type(age) is int:
    print("age is an integer")
# isinstance() — the preferred way (supports inheritance)
number = 2.5
print(isinstance(number, float))        # True
print(isinstance(number, (int, float))) # True — check multiple types
print(isinstance(True, int))            # True — bool is a subclass of int

# Why isinstance() is preferred over type()
# type() does NOT consider inheritance; isinstance() does
print(type(True) is int)        # False — type is exactly bool, not int
print(isinstance(True, int))    # True  — bool IS an int (subclass)

As a rule of thumb, use isinstance() in production code because it respects class inheritance. Use type() for debugging and quick inspection.

 


 

Key Takeaways

Here are the essential points every Python developer should internalize about data types:

  1. Everything is an object. Every value in Python — even int, bool, and None — is an instance of a class. Use type() to inspect any value.
  2. Mutable vs. immutable matters. Lists, dicts, sets, and bytearrays can be changed in place. Strings, tuples, frozensets, and bytes cannot. Understanding this distinction prevents some of the most common Python bugs.
  3. Dynamic typing is a feature, not a flaw. Python infers types at runtime. This gives you flexibility, but you should use clear variable names and type hints (in larger projects) to keep code readable.
  4. Float arithmetic is approximate. Never use == to compare floats. Use math.isclose() or the decimal module for precision-sensitive work.
  5. Use the right type for the job. Need order + mutability? Use a list. Need immutability? Use a tuple. Need fast key-based lookups? Use a dict. Need uniqueness? Use a set.
  6. Type conversion is explicit. Python will not silently convert "5" + 3 for you — use int(), str(), float() to cast deliberately.
  7. Prefer isinstance() over type() for type checking in production code, as it respects class inheritance.

 

Source code on Github




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

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