A for loop is used when you want to iterate over a sequence (like a list, tuple, dictionary, set, or string) or repeat something a specific number of times. It has a predetermined end.
Syntax
for item in sequence:
# Code to execute for each item
In Python, a string is considered a sequence of characters. When you use a for loop on a string, it loops through it character by character.
word = "Python"
for letter in word:
print(letter)
Python has a unique feature: you can pair an else block with a for loop.
The else block executes only if the loop finishes normally (meaning it went through every single item in the sequence without hitting a break statement). If the loop is terminated early by a break, the else block is skipped entirely.
A classic real-world application for a for-else loop in Python is checking whether a number is a prime number.
number = 17
# Prime numbers must be greater than 1
if number > 1:
# Check for factors from 2 up to the number itself
for i in range(2, number):
if (number % i) == 0:
print(f"{number} is not a prime number.")
print(f"Because {i} times {number // i} is {number}.")
break # Found a factor, so stop looping immediately
else:
# This only runs if the loop finished without hitting 'break'
print(f"{number} is a prime number!")
else:
print(f"{number} is not a prime number.")
Another example:
ingredients = ["flour", "sugar", "milk", "butter"]
for ingredient in ingredients:
if ingredient == "peanuts":
print("Danger! This food contains peanuts. Stopping search.")
break
else:
# This runs because the loop checked everything and never hit 'break'
print("Safe to eat! No peanuts found.")
The range function:
The range() function in Python is a built-in tool used to generate a sequence of numbers. It is most commonly used to control the number of iterations in a for loop.
Instead of generating all numbers in memory at once (like a list), range() creates numbers on the fly, making it incredibly efficient.
By default, range() counts upward. It requires a stop value, while the start and step (increment) values are optional.
Syntax: range(start, stop, step)
start (Optional): Where to begin (defaults to 0). Inclusive.
stop (Required): Where to end. Exclusive (stops one step before this number).
step (Optional): The amount to increase by each time (defaults to 1).
range(5) → 0, 1, 2, 3, 4 (Starts at 0, stops before 5)
range(2, 6) → 2, 3, 4, 5 (Starts at 2, stops before 6)
range(1, 10, 2) → 1, 3, 5, 7, 9 (Counts by 2)
Example:
for i in range(7,10):
print(i)
To count downward, you have two straightforward options:
Set the start higher than the stop, and use a negative step.
# Counts down from 5 to 1
for i in range(5, 0, -1):
print(i) # Output: 5, 4, 3, 2, 1
(Remember: The stop value 0 is excluded, so it stops at 1.)
Wrap a standard forward range inside Python's built-in reversed() function. This is often easier to read.
# Flips the normal 1-5 range backward
for i in reversed(range(1, 6)):
print(i) # Output: 5, 4, 3, 2, 1
You will often see range(len(...)) used to loop through a list using its index numbers:
flavors = ["Vanilla", "Chocolate", "Strawberry"]
for i in range(len(flavors)):
print(f"Index {i} is {flavors[i]}")
The while Loop
In Python, a while loop runs a block of code as long as a certain condition remains True.
However, sometimes you need to interrupt the loop's normal flow based on what happens inside the loop. That's where break and continue come in.
Here is the breakdown of how they work, complete with code examples.
The break statement immediately terminates the loop entirely. Python stops executing the loop code and jumps straight to the next line of code below the loop.
Imagine you are counting from 1 to 10, but you want to stop everything the moment you hit 5.
count = 1
while count <= 10:
if count == 5:
print("Found 5! Breaking out of the loop.")
break # This exits the loop completely
print(f"Current number: {count}")
count += 1
print("Loop is finished.")
The continue statement doesn't stop the whole loop; it just skips the rest of the code in the current iteration and jumps straight back to the top to check the condition for the next round.
Let's say you want to print numbers from 1 to 5, but you want to skip the odd numbers.
count = 0
while count < 5:
count += 1
if count % 2 != 0:
print(f"Skipping {count} (it's odd)")
continue # This jumps back to the 'while' line, skipping the print below
print(f"Living on the even side: {count}")
Critical Warning with continue: Always make sure your loop counter (like count += 1) happens before the continue statement. If it's placed after, your variable will never update, and you will get stuck in an infinite loop!
The pass statement is a placeholder. It does absolutely nothing to the execution of the loop—it doesn't break out, and it doesn't skip to the top.
Python requires blocks of code (like loops, functions, or if statements) to have something written inside them. If you leave them empty, Python will throw a SyntaxError. You use pass when you want to write a loop structure but aren't ready to write the actual logic yet.
count = 1
while count <= 3:
if count == 2:
pass # TODO: Handle this special case later!
else:
print(f"Processing number {count}")
count += 1
An infinite loop occurs when the loop's condition never becomes False. The loop will keep running forever until your computer runs out of memory, or you manually force it to stop.
This usually happens by accident when you forget to update your loop counter.
count = 1
while count <= 3:
print("I am stuck!")
# Missing: count += 1
# Because 'count' stays 1 forever, this loop never ends!
Sometimes, infinite loops are created on purpose. If you set the condition directly to True, it creates an intentional infinite loop. You then use a break statement inside to control exactly when to exit.
This is incredibly common for menus or games where you want to keep asking the user for input until they choose to quit.
while True:
user_input = input("Type 'exit' to quit: ")
if user_input == "exit":
print("Goodbye!")
break # This is the ONLY way out of the loop
print(f"You typed: {user_input}")
Python uses two types of indexing:
Positive Indexing: Starts from 0 at the beginning of the string and moves right.
Negative Indexing: Starts from -1 at the end of the string and moves left.
Let's look at the string "PYTHON":
text = "PYTHON"
# Accessing characters using positive indices
print(text[0]) # Output: P
print(text[4]) # Output: O
# Accessing characters using negative indices
print(text[-1]) # Output: N (Last character)
print(text[-4]) # Output: T
Watch out: If you try to access an index that doesn't exist (e.g., text[10]), Python will throw an IndexError: string index out of range.
Slicing allows you to grab a slice of your string using the syntax: string[start:stop:step]
start: The index where the slice begins (included). Default is 0.
stop: The index where the slice ends (excluded—Python stops right before this index).
step: (Optional) Determines the increment between characters. Default is 1.
text = "PYTHON"
# Slice from index 1 up to (but not including) index 4
print(text[1:4]) # Output: YTH
# If you leave out the start, it defaults to the beginning (0)
print(text[:4]) # Output: PYTH
# If you leave out the stop, it goes all the way to the end
print(text[2:]) # Output: THON
# Using negative indices in slicing
print(text[-4:-1]) # Output: THO
text = "PYTHON"
# Get every second character
print(text[0:6:2]) # Output: PTO
# The famous Python trick: Reverse a string using a negative step
print(text[::-1]) # Output: NOHTYP
# you could also use reversed()
Python lists are incredibly versatile, largely because they are dynamic and can hold any mix of data types.
Ordered: Lists maintain the exact order in which you insert items.
Mutable: You can change, add, or remove items after the list is created.
Allow Duplicates: Because items are accessed by their position, you can have multiple identical items.
Heterogeneous: A single list can hold different data types at the same time (integers, strings, booleans, or even other lists).
Lists are defined by placing comma-separated values inside square brackets [].
# A list of strings
fruits = ["apple", "banana", "cherry"]
# A list of mixed data types
mixed_list = ["Python", 2026, True, 3.14]
# An empty list
empty_list = []
Unlike arrays in many other languages, Python lists don't care what you put inside them. A single list can hold integers, strings, floats, booleans, and even other lists all at once.
Python
# A single list with diverse data types
dynamic_list = [42, "Hello", 3.14, True, [1, 2, 3]]
print(dynamic_list)
# Output: [42, 'Hello', 3.14, True, [1, 2, 3]]
A list is created using square brackets []. To add a single element to the very end of a list, you use the .append() method. This modifies the original list in place.
# Creating a list
fruits = ["apple", "banana", "cherry"]
# Adding a single item to the end
fruits.append("date")
print(fruits)
# Output: ['apple', 'banana', 'cherry', 'date']
Just like strings, lists use 0-based indexing. You can access single items or slices of a list.
languages = ["Python", "Java", "C++", "JavaScript"]
# Accessing by index
print(languages[0]) # Output: Python
print(languages[-1]) # Output: JavaScript (last item)
# Slicing (extracting a sub-list)
print(languages[1:3]) # Output: ['Java', 'C++'] (stops before index 3)
Because lists are mutable, you can change an item directly using its index:
shopping_list = ["milk", "bread", "eggs"]
shopping_list[1] = "croissants"
print(shopping_list) # Output: ['milk', 'croissants', 'eggs']
Slicing allows you to access a specific chunk of a list using the syntax list[start:stop]. What makes Python powerful is that you can also assign new values to a slice to modify multiple elements at once.
The replacement list doesn't even have to be the same length as the slice you are replacing!
numbers = [10, 20, 30, 40, 50]
# Replace elements at index 1 and 2 (20 and 30)
numbers[1:3] = [99, 100]
print(numbers)
# Output: [10, 99, 100, 40, 50]
# Replace a slice with a smaller list (shrinks the list)
numbers[1:4] = [0]
print(numbers)
# Output: [10, 0, 50]
# Insert elements without deleting anything
numbers[1:1] = [1, 2]
print(numbers)
# Output: [10, 1, 2, 0, 50]
The simplest way to concatenate two or more lists is by using the + operator. This joins the lists together and returns a completely new list, leaving the original lists untouched.
list1 = ["apple", "banana"]
list2 = ["cherry", "date"]
# Concatenate into a new list
combined_list = list1 + list2
print(combined_list)
# Output: ['apple', 'banana', 'cherry', 'date']
print(list1)
# Output: ['apple', 'banana'] (Original remains unchanged)
If you want to append all the elements of one list onto the end of an existing list without creating a new one, use .extend().
Note the difference from .append(): > * .append(list2) would add list2 as a single item (creating a nested list: ['apple', 'banana', ['cherry', 'date']]).
.extend(list2) unpacks the items and adds them individually.
list1 = ["apple", "banana"]
list2 = ["cherry", "date"]
# Modify list1 in-place
list1.extend(list2)
print(list1)
# Output: ['apple', 'banana', 'cherry', 'date']
The += operator is a shorthand equivalent to the .extend() method. It modifies the original list in place.
list1 = ["apple", "banana"]
list2 = ["cherry", "date"]
list1 += list2
print(list1)
# Output: ['apple', 'banana', 'cherry', 'date']
Python gives you a few different ways to remove items depending on what you know about them (the value vs. the position):
.remove(value): Removes the first occurrence of a specific value.
.pop(index): Removes and returns the item at a specific index (defaults to the last item).
del statement: Deletes an item or a slice by index.
items = ["backpack", "map", "compass", "map", "flashlight"]
items.remove("map") # Removes the first "map"
print(items) # Output: ['backpack', 'compass', 'map', 'flashlight']
popped_item = items.pop(1) # Removes 'compass'
print(popped_item) # Output: 'compass'
print(items) # Output: ['backpack', 'map', 'flashlight']
del items[0] # Deletes 'backpack'
print(items) # Output: ['map', 'flashlight']
If you want to wipe the list completely clean but keep the empty list structure alive, use .clear().
items = [1, 2, 3]
items.clear()
print(items)
# Output: []
To remove that chunk completely, you can assign an empty list [] to it.
numbers = [10, 20, 30, 40, 50, 60]
# Remove the elements from index 1 up to (but not including) 4
numbers[1:4] = []
print(numbers)
# Output: [10, 50, 60]
Because lists can hold any data type, they can easily hold other lists. This is perfect for representing grids, matrices, or coordinate systems. To access items, you chain square brackets [row][column].
# A 3x3 matrix (nested list)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Accessing the element '6' (Row index 1, Column index 2)
print(matrix[1][2]) # Output: 6
# Modifying a nested element
matrix[0][0] = 99
print(matrix[0]) # Output: [99, 2, 3]
List comprehension provides a sleek, one-line syntax to create new lists based on existing iterables. It replaces bulky for loops.
Syntax Template: [expression for item in iterable if condition]
# Traditional way
squares = []
for x in range(5):
squares.append(x**2)
# The List Comprehension way
squares_comp = [x**2 for x in range(5)]
print(squares_comp) # Output: [0, 1, 4, 9, 16]
# With a condition (even numbers only)
even_squares = [x**2 for x in range(10) if x % 2 == 0]
print(even_squares) # Output: [0, 4, 16, 36, 64]