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: | int, float, complex |
| Sequence Types: | list, tuple, range |
| Mapping Type: | dict |
| Set Types: | set, frozenset |
| Boolean Type: | bool |
| Binary Types: | bytes, bytearray, memoryview |
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:
int, bool, and None — is an instance of a class. Use type() to inspect any value.== to compare floats. Use math.isclose() or the decimal module for precision-sensitive work."5" + 3 for you — use int(), str(), float() to cast deliberately.isinstance() over type() for type checking in production code, as it respects class inheritance.