Lecture#
Loops#
Repeating actions with for and while loops.
Create a file called loops_demo.py to follow along with the examples below.
The range() Function
The range() function generates an immutable sequence of integers. It is one of the most commonly used tools for looping in Python.
Note
Syntax: range(stop) or range(start, stop) or range(start, stop, step)
start— Starting value (default: 0, inclusive)stop— Ending value (exclusive, never included!)step— Increment between values (default: 1)
# range() returns a range object, not a list
r = range(5)
print(r) # range(0, 5)
print(type(r)) # <class 'range'>
# Convert to list to see all values
print(list(range(5))) # [0, 1, 2, 3, 4]
Warning
The stop value is never included. range(5) gives [0, 1, 2, 3, 4], not [0, 1, 2, 3, 4, 5]!
Basic range() Usage
# range(stop) - generates 0 to stop-1
print(list(range(5))) # [0, 1, 2, 3, 4]
# range(start, stop) - generates start to stop-1
print(list(range(2, 7))) # [2, 3, 4, 5, 6]
# range(start, stop, step) - with custom increment
print(list(range(0, 10, 2))) # [0, 2, 4, 6, 8]
print(list(range(1, 10, 2))) # [1, 3, 5, 7, 9]
# Negative step - counting backwards
print(list(range(5, 0, -1))) # [5, 4, 3, 2, 1]
print(list(range(10, 0, -2))) # [10, 8, 6, 4, 2]
range() Tricks: Indexing, Slicing, and Membership
# Get the length of a range without converting to list
r = range(0, 1000000)
print(len(r)) # 1000000 (instant, no memory used!)
# Check membership efficiently - O(1) constant time!
print(500000 in r) # True (very fast!)
print(999999 in r) # True
print(1000000 in r) # False (stop value not included)
# Indexing works on range objects
r = range(10, 20)
print(r[0]) # 10 (first element)
print(r[-1]) # 19 (last element)
print(r[5]) # 15
# Slicing returns a new range object
print(r[2:5]) # range(12, 15)
print(list(r[2:5])) # [12, 13, 14]
range() Tricks: Memory Efficiency
range() is a lazy iterator — it generates values on demand, not all at once.
import sys
# A list stores all values in memory
big_list = list(range(1000000))
print(sys.getsizeof(big_list)) # ~8,000,000+ bytes (8 MB)
# A range object only stores start, stop, step
big_range = range(1000000)
print(sys.getsizeof(big_range)) # 48 bytes (always!)
# Even a massive range uses the same tiny amount of memory
huge_range = range(1000000000000) # One trillion!
print(sys.getsizeof(huge_range)) # Still just 48 bytes
Tip
Best Practice: Use range() directly in loops. Only convert to a list if you actually need to store all values.
The for Loop
The for loop iterates over any iterable object (strings, ranges, and more).
# Iterate over a string
message = "Hello"
for char in message:
print(char, end=" ") # H e l l o
print() # Newline
# Iterate over a range
for i in range(5):
print(i, end=" ") # 0 1 2 3 4
print()
# Using range for repetition
for _ in range(3): # _ indicates we don't need the value
print("Robot activated!")
Combining range() with Strings
message = "Python"
# Access characters by index
for i in range(len(message)):
print(f"Index {i}: {message[i]}")
# Index 0: P
# Index 1: y
# ...
# Print every other character
for i in range(0, len(message), 2):
print(message[i], end="") # Pto
print()
# Print string in reverse using range
for i in range(len(message) - 1, -1, -1):
print(message[i], end="") # nohtyP
The enumerate() Function
When you need both the index and value, use enumerate() instead of range(len(...)).
message = "Robot"
# Less Pythonic way
for i in range(len(message)):
print(f"{i}: {message[i]}")
# More Pythonic way with enumerate()
for index, char in enumerate(message):
print(f"{index}: {char}")
# Start counting from 1 instead of 0
for index, char in enumerate(message, start=1):
print(f"Character {index}: {char}")
# Character 1: R
# Character 2: o
# ...
The while Loop
The while loop repeats as long as a condition is True.
# Basic while loop - counting
count = 0
while count < 5:
print(count, end=" ")
count += 1 # Don't forget to update!
# Output: 0 1 2 3 4
# Building a string character by character
result = ""
i = 0
word = "Hello"
while i < len(word):
result += word[i].upper()
i += 1
print(result) # HELLO
Warning
Always ensure the loop condition will eventually become False, or you’ll create an infinite loop!
Loop Control: break, continue, and else
Exit the loop immediately.
# Find first vowel in a string
word = "python"
for char in word:
if char in "aeiou":
print(f"First vowel: {char}")
break
# Output: First vowel: o
Skip to the next iteration.
# Print only consonants
word = "hello"
for char in word:
if char in "aeiou":
continue
print(char, end=" ")
# Output: h l l
Runs if the loop completes without break.
# Search for a character
word = "robot"
target = "x"
for char in word:
if char == target:
print(f"Found {target}!")
break
else:
print(f"{target} not found")
# Output: x not found
Note
The else clause after a loop is a unique Python feature — useful for search patterns!
Iterables#
Objects that can be traversed element by element.
What Are Iterables?
An iterable is an object that can be “iterated over”, meaning its elements can be accessed one at a time in sequence.
Lists
Strings
Tuples
Dictionaries (keys, values, items)
Sets and frozensets
Files, range objects, generators
# We've already seen iterating over strings and ranges
for char in "Hi":
print(char, end=" ") # H i
for num in range(3):
print(num, end=" ") # 0 1 2
# Soon we'll iterate over lists, dicts, and sets!
In-Place vs Out-of-Place Operations
Modify the original object directly
Do not create a new object
Common with mutable types
Usually return
None
fruits = ["apple", "banana"]
result = fruits.append("kiwi")
print(result) # None
print(fruits) # ['apple', 'banana', 'kiwi']
Create and return a new object
Leave the original unchanged
Required for immutable types
Return the new value
text = "hello"
upper_text = text.upper()
print(text) # hello
print(upper_text) # HELLO
Warning
Always check whether a method modifies in-place or returns a new object. Assigning the result of an in-place operation often leads to bugs!
Lists#
Python’s versatile, ordered, mutable sequence type.
Create a file called lists_demo.py to follow along with the examples below.
The List Type
A Python list (list) is an ordered and mutable sequence of objects.
Name |
Type |
Example |
Mutable |
Ordered |
|---|---|---|---|---|
List |
|
|
Yes |
Yes |
# Lists can contain any objects
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, None]
nested = [[1, 2], [3, 4], [5, 6]]
# Lists with same items in different order are different
a = [1, 2, 3]
b = [3, 2, 1]
print(a == b) # False
Creating Lists
There are several ways to create lists:
Square brackets with comma-separated values:
fruits = ["apple", "banana", "cherry"]
empty = []
``list()`` constructor with an iterable:
chars = list("hello") # ['h', 'e', 'l', 'l', 'o']
nums = list(range(5)) # [0, 1, 2, 3, 4]
empty = list() # []
List comprehension:
squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
Repetition operator:
zeros = [0] * 5 # [0, 0, 0, 0, 0]
Indexing and Slicing
Lists support the same indexing and slicing operations as strings.
fruits = ["apple", "banana", "cherry", "date", "elderberry"]
# Indexing
print(fruits[0]) # apple
print(fruits[-1]) # elderberry
# Slicing
print(fruits[1:4]) # ['banana', 'cherry', 'date']
print(fruits[::2]) # ['apple', 'cherry', 'elderberry']
# Modifying elements (lists are mutable!)
fruits[0] = "apricot"
print(fruits) # ['apricot', 'banana', 'cherry', 'date', 'elderberry']
# Modifying slices
fruits[1:3] = ["blueberry"]
print(fruits) # ['apricot', 'blueberry', 'date', 'elderberry']
Common List Methods
Adding elements:
fruits = ["apple"]
fruits.append("banana") # Add to end: ['apple', 'banana']
fruits.extend(["kiwi", "mango"]) # Add multiple: ['apple', 'banana', 'kiwi', 'mango']
fruits.insert(1, "cherry") # Insert at index: ['apple', 'cherry', 'banana', ...]
Removing elements:
fruits = ["apple", "banana", "apple", "cherry"]
fruits.remove("apple") # Remove first occurrence: ['banana', 'apple', 'cherry']
last = fruits.pop() # Remove and return last: 'cherry'
first = fruits.pop(0) # Remove and return at index: 'banana'
fruits.clear() # Remove all: []
Searching and sorting:
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(nums.index(4)) # 2 (first occurrence)
print(nums.count(1)) # 2 (count occurrences)
nums.sort() # In-place sort: [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True) # Descending: [9, 6, 5, 4, 3, 2, 1, 1]
nums.reverse() # In-place reverse
# sorted() returns a new list (out-of-place)
original = [3, 1, 4]
new_list = sorted(original) # [1, 3, 4], original unchanged
List Comprehensions
List comprehensions provide a compact way to create lists from iterables.
Note
Syntax: [expression for item in iterable if condition]
# Basic comprehension
squares = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
# With condition (filtering)
evens = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
# With transformation
words = ["hello", "world"]
upper = [w.upper() for w in words] # ['HELLO', 'WORLD']
# Conditional expression (ternary in comprehension)
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']
Shallow vs Deep Copy
Copies top-level elements
Nested objects share references
Methods:
copy(),list(), slice[:]
from copy import copy
a = [1, [2, 3]]
b = copy(a) # or a.copy() or a[:]
b[0] = 99
b[1].append(4)
print(a) # [1, [2, 3, 4]]
print(b) # [99, [2, 3, 4]]
Recursively copies all nested objects
Completely independent copy
Method:
deepcopy()
from copy import deepcopy
a = [1, [2, 3]]
b = deepcopy(a)
b[0] = 99
b[1].append(4)
print(a) # [1, [2, 3]]
print(b) # [99, [2, 3, 4]]
Warning
Using = does NOT copy a list! It creates an alias (both names point to the same object).
Tuples#
Immutable sequences for fixed collections of items.
Create a file called tuples_demo.py to follow along with the examples below.
The Tuple Type
A Python tuple (tuple) is an ordered and immutable sequence of objects.
Name |
Type |
Example |
Mutable |
Ordered |
|---|---|---|---|---|
Tuple |
|
|
No |
Yes |
# Creating tuples
coordinates = (3.5, 7.2)
rgb = (255, 128, 0)
mixed = (1, "hello", [1, 2]) # Can contain mutable objects
# Important: commas make the tuple, not parentheses!
single = (42,) # Tuple with one element
not_tuple = (42) # Just an integer!
Note
Tuples are ideal for representing fixed collections like coordinates, RGB values, or database records.
Creating Tuples
Parentheses (optional but recommended):
point = (3, 4)
empty = ()
Comma-separated values (tuple packing):
point = 3, 4 # Same as (3, 4)
singleton = 42, # Tuple with one element
``tuple()`` constructor:
from_list = tuple([1, 2, 3]) # (1, 2, 3)
from_string = tuple("abc") # ('a', 'b', 'c')
from_range = tuple(range(3)) # (0, 1, 2)
Warning
For a single-element tuple, you must include the trailing comma: (42,) not (42)
Tuple Unpacking
Tuple unpacking assigns each element to a separate variable.
# Basic unpacking
coordinates = (3.5, 7.2)
x, y = coordinates
print(f"x={x}, y={y}") # x=3.5, y=7.2
# Swap values elegantly
a, b = 10, 20
a, b = b, a # Now a=20, b=10
# Ignore values with underscore
point = (1, 2, 3)
x, _, z = point # Ignore the y-coordinate
# Extended unpacking with *
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
Tuples Are Immutable
You cannot modify tuple elements after creation.
point = (3, 4, 5)
# These will raise TypeError:
# point[0] = 10
# del point[1]
# However, mutable objects inside tuples CAN be modified!
data = (1, 2, [3, 4])
data[2].append(5) # OK!
print(data) # (1, 2, [3, 4, 5])
# But you cannot replace the list itself
# data[2] = [10, 20] # TypeError!
Note
Why use tuples? They’re hashable (can be dict keys), faster than lists, and signal intent that data shouldn’t change.
Dictionaries#
Key-value mappings for fast lookups.
Create a file called dictionaries_demo.py to follow along with the examples below.
The Dictionary Type
A Python dictionary (dict) is an ordered (since Python 3.7) and mutable mapping of unique keys to values.
Name |
Type |
Example |
Mutable |
Ordered |
|---|---|---|---|---|
Dictionary |
|
|
Yes |
Yes |
# Robot configuration dictionary
robot = {
"name": "TurtleBot3",
"type": "mobile",
"max_speed": 0.26,
"sensors": ["lidar", "camera", "imu"]
}
print(robot["name"]) # TurtleBot3
print(robot["max_speed"]) # 0.26
Creating Dictionaries
Curly braces with key-value pairs:
d = {"name": "Alice", "age": 30}
empty = {}
``dict()`` constructor:
d = dict(name="Alice", age=30) # Keyword arguments
d = dict([("name", "Alice"), ("age", 30)]) # List of tuples
d = dict({"name": "Alice"}, age=30) # Mixed
Dictionary comprehension:
squares = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
``dict.fromkeys()``:
keys = ["a", "b", "c"]
d = dict.fromkeys(keys, 0) # {'a': 0, 'b': 0, 'c': 0}
Accessing and Modifying
robot = {"name": "UR5", "joints": 6}
# Accessing values
print(robot["name"]) # UR5
print(robot.get("speed")) # None (no KeyError!)
print(robot.get("speed", 0)) # 0 (default value)
# Adding/modifying items
robot["speed"] = 1.0 # Add new key
robot["joints"] = 7 # Modify existing
# Removing items
del robot["speed"] # Remove key (KeyError if missing)
value = robot.pop("joints") # Remove and return value
robot.clear() # Remove all items
# Check if key exists
if "name" in robot:
print(robot["name"])
Iterating Over Dictionaries
robot = {"name": "TurtleBot", "type": "mobile", "speed": 0.26}
# Iterate over keys (default)
for key in robot:
print(key, end=" ") # name type speed
# Iterate over values
for value in robot.values():
print(value, end=" ") # TurtleBot mobile 0.26
# Iterate over key-value pairs
for key, value in robot.items():
print(f"{key}: {value}")
# name: TurtleBot
# type: mobile
# speed: 0.26
Note
View objects (keys(), values(), items()) are dynamic — they reflect changes to the dictionary.
Sets#
Unordered collections of unique elements.
Create a file called sets_demo.py to follow along with the examples below.
The Set Type
A Python set (set) is an unordered, mutable collection that contains no duplicate elements.
Name |
Type |
Example |
Mutable |
Ordered |
|---|---|---|---|---|
Set |
|
|
Yes |
No |
# Sets automatically remove duplicates
numbers = {1, 2, 2, 3, 3, 3}
print(numbers) # {1, 2, 3}
# Creating sets
fruits = {"apple", "banana", "cherry"}
from_list = set([1, 2, 2, 3]) # {1, 2, 3}
empty_set = set() # NOT {} (that's an empty dict!)
Warning
Use set() to create an empty set. {} creates an empty dictionary!
Mathematical Set Operations
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
# Union: elements in either set
print(a | b) # {1, 2, 3, 4, 5, 6}
print(a.union(b)) # Same result
# Intersection: elements in both sets
print(a & b) # {3, 4}
print(a.intersection(b)) # Same result
# Difference: elements in a but not in b
print(a - b) # {1, 2}
print(a.difference(b)) # Same result
# Symmetric difference: elements in either but not both
print(a ^ b) # {1, 2, 5, 6}
print(a.symmetric_difference(b)) # Same result
Modifying Sets
Adding elements:
s = {1, 2, 3}
s.add(4) # {1, 2, 3, 4}
s.update([5, 6]) # {1, 2, 3, 4, 5, 6}
Removing elements:
s = {1, 2, 3, 4, 5}
s.remove(3) # {1, 2, 4, 5} - raises KeyError if missing
s.discard(99) # No error if missing
x = s.pop() # Remove and return arbitrary element
s.clear() # Remove all: set()
Note
Use cases for sets: Removing duplicates, membership testing, and finding common/unique elements between collections.
Summary#
Loops —
for,while,break/continue/elserange() — Lazy iterator; memory efficient; indexing, slicing, membership
Iterables — Objects that can be traversed; in-place vs out-of-place
Lists — Ordered, mutable sequences; comprehensions; copy methods
Tuples — Ordered, immutable sequences; unpacking
Dictionaries — Key-value mappings; view objects
Sets — Unordered unique collections; set operations
Type |
Mutable |
Ordered |
Duplicates |
|---|---|---|---|
|
Yes |
Yes |
Yes |
|
No |
Yes |
Yes |
|
Yes |
Yes |
Keys: No |
|
Yes |
No |
No |
Note
Reminder: Review and experiment with all provided code before next class.
Preview: What’s Next in L4#
Defining and calling functions
Parameters and arguments
Return values
Scope and namespaces
Type hints
Docstrings
Note
Today’s lecture gives you the data structures and iteration tools that you will use constantly when writing functions in L4 and beyond.