Arrays and Strings in Python — Beginner-Friendly Notes

Table Of Content
- 1. What Is a Data Structure?
- 2. What Is an Array?
- 3. Array Indexes
- 4. Reading and Updating Array Elements
- 5. Traversing an Array
- 6. Searching an Array
- 7. Adding Elements
- 8. Removing Elements
- 9. append() vs extend()
- 10. Array Slicing
- 11. What Is a String?
- 12. Strings Are Immutable
- 13. Traversing a String
- 14. String Slicing
- 15. Comparing Strings
- 16. Common String Operations
- 17. String Concatenation
- EXAMPLE 1 — Find the Largest Element
- EXAMPLE 2 — Reverse an Array
- EXAMPLE 3 — Check Whether a String Is a Palindrome
- EXAMPLE 4 — Count Character Frequencies
- Array vs String
- Common Mistakes
- Complexity Summary
- Quick Rules
- Final Summary
Arrays and Strings in Python — Beginner-Friendly Notes
These notes cover the two most basic data structures in Python: lists (used as arrays) and strings. The focus is on how they store data, how we access them, and what each common operation costs in Big-O terms.
1. What Is a Data Structure?
A data structure is a way of organizing and storing data so that a program can use it efficiently.
Suppose we need to store five student marks.
Using separate variables:
mark1 = 75
mark2 = 82
mark3 = 68
mark4 = 90
mark5 = 77This becomes difficult to manage.
Using a collection:
marks = [75, 82, 68, 90, 77]Now the values are stored together, and we can process them using indexes and loops.
A data structure organizes related data and defines how we can access, update, add, or remove it.
Common data structures include:
- Arrays
- Strings
- Linked lists
- Stacks
- Queues
- Hash tables
- Trees
- Graphs
This post focuses on the two most basic structures: arrays and strings.
2. What Is an Array?
An array stores multiple values in an ordered sequence.
numbers = [10, 20, 30, 40]Conceptually:
Index: 0 1 2 3
┌────┬────┬────┬────┐
Value: │ 10 │ 20 │ 30 │ 40 │
└────┴────┴────┴────┘Each value is called an element, and each element has an index.
Python indexes begin at 0:
print(numbers[0]) # 10
print(numbers[1]) # 20
print(numbers[3]) # 40The first element is at index
0, not index1.
Python note
Python's built-in list is not a traditional fixed-size array. It is a dynamic array, meaning its size can grow or shrink.
In beginner-level Python code, lists are commonly used as arrays:
numbers = [10, 20, 30]3. Array Indexes
Given:
fruits = ["apple", "banana", "mango", "orange"]The indexes are:
Index: 0 1 2 3
┌─────────┬──────────┬─────────┬──────────┐
Value: │ "apple" │ "banana" │ "mango" │ "orange" │
└─────────┴──────────┴─────────┴──────────┘Accessing elements:
print(fruits[0]) # apple
print(fruits[2]) # mangoNegative indexing
Python also supports negative indexes:
Positive: 0 1 2 3
Negative: -4 -3 -2 -1
┌─────────┬──────────┬─────────┬──────────┐
Value: │ "apple" │ "banana" │ "mango" │ "orange" │
└─────────┴──────────┴─────────┴──────────┘Examples:
print(fruits[-1]) # orange
print(fruits[-2]) # mango-1 means the last element.
4. Reading and Updating Array Elements
Reading an element
numbers = [10, 20, 30]
value = numbers[1]
print(value) # 20Accessing an element by index usually takes:
Time = O(1)The program can directly locate the requested position.
Updating an element
numbers[1] = 99
print(numbers)Output:
[10, 99, 30]Updating a known index also usually takes:
Time = O(1)Direct index access and update are fast because the position is already known.
5. Traversing an Array
Traversing means visiting each element.
numbers = [10, 20, 30, 40]
for number in numbers:
print(number)Output:
10
20
30
40If the array contains n elements, the loop visits all n elements:
Time = O(n)Traversing with indexes
for index in range(len(numbers)):
print(index, numbers[index])Output:
0 10
1 20
2 30
3 40Use direct traversal when only the value is needed:
for number in numbers:
print(number)Use enumerate() when both the index and value are needed:
for index, number in enumerate(numbers):
print(index, number)6. Searching an Array
Suppose we want to find 30:
numbers = [10, 20, 30, 40]A linear search checks elements one by one:
def linear_search(arr, target):
for index, value in enumerate(arr):
if value == target:
return index
return -1Calling:
print(linear_search(numbers, 30))Output:
2Execution:
10 == 30? No
20 == 30? No
30 == 30? Yes → return index 2In the worst case, the target is at the end or does not exist:
Time = O(n)7. Adding Elements
Append to the end
numbers = [10, 20, 30]
numbers.append(40)
print(numbers)Output:
[10, 20, 30, 40]Appending to a Python list is usually:
O(1) amortizedOccasionally, Python must allocate a larger internal array and copy the existing elements. But averaged across many appends, the cost is considered constant.
Insert at a position
numbers.insert(1, 15)
print(numbers)Output:
[10, 15, 20, 30, 40]To insert 15 at index 1, later elements must move right:
Before: [10, 20, 30, 40]
↓ shift right
After: [10, 15, 20, 30, 40]Therefore:
Time = O(n)8. Removing Elements
Remove the last element
numbers = [10, 20, 30]
removed = numbers.pop()
print(removed) # 30
print(numbers) # [10, 20]Removing the last element is usually:
Time = O(1)Remove by index
numbers = [10, 20, 30, 40]
numbers.pop(1)
print(numbers)Output:
[10, 30, 40]Later elements must shift left:
Time = O(n)Remove by value
numbers.remove(30)Python first searches for the value and then shifts later elements:
Time = O(n)9. append() vs extend()
append() adds its argument as one element:
numbers = [1, 2]
numbers.append([3, 4])
print(numbers)Output:
[1, 2, [3, 4]]The nested list [3, 4] becomes one new element.
extend() adds each element separately:
numbers = [1, 2]
numbers.extend([3, 4])
print(numbers)Output:
[1, 2, 3, 4]The difference:
append([3, 4]) → add the entire list as one element
extend([3, 4]) → add 3 and 4 separately10. Array Slicing
Slicing extracts part of an array:
numbers = [10, 20, 30, 40, 50]
print(numbers[1:4])Output:
[20, 30, 40]The general syntax is:
array[start:stop:step]The start index is included, but the stop index is excluded:
numbers[1:4]
↑ ↑
include 1
exclude 4More examples:
numbers[:3] # [10, 20, 30]
numbers[2:] # [30, 40, 50]
numbers[::2] # [10, 30, 50]
numbers[::-1] # [50, 40, 30, 20, 10]A slice creates a new list containing the selected elements.
If the slice contains k elements:
Time = O(k)
Space = O(k)11. What Is a String?
A string is an ordered sequence of characters.
text = "hello"Conceptually:
Index: 0 1 2 3 4
┌────┬────┬────┬────┬────┐
Value: │ h │ e │ l │ l │ o │
└────┴────┴────┴────┴────┘Characters can be accessed using indexes:
print(text[0]) # h
print(text[1]) # e
print(text[-1]) # oAccessing a character by index takes:
Time = O(1)12. Strings Are Immutable
Python lists are mutable. Their elements can be changed:
numbers = [10, 20, 30]
numbers[1] = 99Strings are immutable. Their characters cannot be changed directly:
text = "hello"
text[0] = "H"This produces an error:
TypeError: 'str' object does not support item assignmentTo create "Hello", we must make a new string:
text = "H" + text[1:]
print(text)Output:
HelloMutable means an object can be changed after creation. Immutable means changing it requires creating a new object.
13. Traversing a String
A string can be traversed like an array:
text = "hello"
for character in text:
print(character)Output:
h
e
l
l
oFor a string containing n characters:
Time = O(n)With indexes:
for index, character in enumerate(text):
print(index, character)14. String Slicing
String slicing follows the same rules as list slicing:
text = "python"
print(text[0:3]) # pyt
print(text[2:]) # thon
print(text[:4]) # pyth
print(text[::-1]) # nohtypBecause strings are immutable, slicing creates a new string.
For a slice containing k characters:
Time = O(k)
Space = O(k)15. Comparing Strings
Strings can be compared using:
word1 = "hello"
word2 = "hello"
print(word1 == word2) # TruePython compares characters from left to right.
print("apple" == "apply") # FalseThe first four characters match:
a == a
p == p
p == p
l == lThe final characters differ:
e != yIn the worst case, Python may compare all characters:
Time = O(n)16. Common String Operations
text = "hello world"Length
len(text) # 11Convert to uppercase
text.upper() # "HELLO WORLD"Convert to lowercase
text.lower() # "hello world"Check prefix or suffix
text.startswith("hello") # True
text.endswith("world") # TrueFind a substring
text.find("world") # 6It returns -1 when the substring is not found.
Replace text
text.replace("world", "Python")Result:
"hello Python"These methods return new strings because the original string cannot be modified.
17. String Concatenation
Concatenation means joining strings:
first = "Hello"
second = "World"
result = first + " " + second
print(result)Output:
Hello WorldBecause strings are immutable, concatenation creates a new string.
Repeated concatenation inside a loop can be inefficient:
result = ""
for word in words:
result += wordAs the string grows, Python may repeatedly copy its contents.
A better approach for many strings is:
result = "".join(words)Example:
words = ["Python", "is", "fun"]
sentence = " ".join(words)
print(sentence)Output:
Python is funEXAMPLE 1 — Find the Largest Element
def find_largest(arr):
largest = arr[0]
for value in arr[1:]:
if value > largest:
largest = value
return largestCalling:
find_largest([4, 9, 2, 7])Execution:
largest = 4
compare 9 with 4 → largest = 9
compare 2 with 9 → unchanged
compare 7 with 9 → unchangedResult:
9Complexity:
Time = O(n)
Space = O(1)Python note: arr[1:] creates a copy, which technically uses extra space. To keep auxiliary space at O(1), use indexes:
def find_largest(arr):
largest = arr[0]
for index in range(1, len(arr)):
if arr[index] > largest:
largest = arr[index]
return largestEXAMPLE 2 — Reverse an Array
Using slicing
numbers = [1, 2, 3, 4]
reversed_numbers = numbers[::-1]Result:
[4, 3, 2, 1]Complexity:
Time = O(n)
Space = O(n)Slicing creates a new array.
Reversing in place
def reverse_array(arr):
left = 0
right = len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1Execution:
[1, 2, 3, 4]
↑ ↑
swap 1 and 4
[4, 2, 3, 1]
↑ ↑
swap 2 and 3
[4, 3, 2, 1]Complexity:
Time = O(n)
Space = O(1)This approach uses the two-pointer technique.
EXAMPLE 3 — Check Whether a String Is a Palindrome
A palindrome reads the same forward and backward:
"madam"
"racecar"
"level"A simple solution:
def is_palindrome(text):
return text == text[::-1]A two-pointer solution:
def is_palindrome(text):
left = 0
right = len(text) - 1
while left < right:
if text[left] != text[right]:
return False
left += 1
right -= 1
return TrueFor "level":
l e v e l
↑ ↑ l == l
↑ ↑ e == e
↑ pointers meetResult:
TrueComplexity:
Time = O(n)
Space = O(1)EXAMPLE 4 — Count Character Frequencies
def count_characters(text):
frequency = {}
for character in text:
frequency[character] = frequency.get(character, 0) + 1
return frequencyCalling:
count_characters("banana")Result:
{
"b": 1,
"a": 3,
"n": 2
}The dictionary stores each character and its count.
Complexity:
Time = O(n)
Space = O(k)Here, k is the number of distinct characters.
Array vs String
| Array/list | String |
|---|---|
| Ordered collection of elements | Ordered collection of characters |
| Can contain different data types | Contains characters |
| Mutable | Immutable |
| Elements can be updated directly | Characters cannot be updated directly |
Example: [10, 20, 30] | Example: "hello" |
Common Mistakes
Mistake 1 — Using the wrong index
numbers = [10, 20, 30]
print(numbers[3])Valid indexes are:
0, 1, 2Therefore, numbers[3] produces:
IndexError: list index out of rangeThe last valid index is:
len(numbers) - 1Mistake 2 — Changing a string directly
Incorrect:
text = "hello"
text[0] = "H"Correct:
text = "H" + text[1:]Mistake 3 — Confusing append() and extend()
numbers.append([3, 4])Result:
[1, 2, [3, 4]]But:
numbers.extend([3, 4])Result:
[1, 2, 3, 4]Mistake 4 — Modifying a list while traversing it
This can skip elements:
numbers = [1, 2, 2, 3]
for number in numbers:
if number == 2:
numbers.remove(number)A safer approach is to create a new list:
numbers = [
number
for number in numbers
if number != 2
]Complexity Summary
| Operation | Python list | String |
|---|---|---|
| Access by index | O(1) | O(1) |
| Update by index | O(1) | Not allowed |
| Traverse | O(n) | O(n) |
| Search | O(n) | O(n) |
| Append to end | O(1) amortized | Not applicable |
| Insert/remove at beginning | O(n) | Not applicable |
| Remove last element | O(1) | Not applicable |
Slice k elements | O(k) | O(k) |
| Reverse using slicing | O(n) | O(n) |
| Concatenation | Depends on added elements | Creates a new string |
Quick Rules
-
Python lists behave like dynamic arrays.
-
Array and string indexes start at
0. -
Negative indexes count from the end:
-1 → last element
-2 → second-last element- Accessing a known index is usually:
O(1)- Traversing or linearly searching
nelements is:
O(n)- Inserting or removing near the beginning of an array requires shifting elements:
O(n)-
Lists are mutable, but strings are immutable.
-
Slicing creates a new list or string.
-
append()adds one object;extend()adds each element from an iterable. -
Two pointers are useful for reversing arrays and checking palindromes.
Final Summary
Arrays and strings are ordered data structures that use indexes to access their contents.
Python uses lists as dynamic arrays:
numbers = [10, 20, 30]A string is an immutable sequence of characters:
text = "hello"The most important differences are:
List → mutable
String → immutableBoth structures support indexing, traversal, searching, and slicing. Understanding their operation costs is important:
Direct access → O(1)
Full traversal → O(n)
Linear search → O(n)
Insert at beginning → O(n)
Slice k elements → O(k)Arrays and strings also introduce important problem-solving techniques such as linear traversal, frequency counting, and two pointers. These techniques appear repeatedly in coding interviews and more advanced data structures.