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.

Sequence Types
  • Lists

  • Strings

  • Tuples

Other Iterables
  • 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
In-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']
Out-of-Place Operations
  • 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

list

[1, 'hello', 3.5]

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
Shallow 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]]
Example of a shallow copy Example of a shallow copy
Deep Copy
  • 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]]
Example of a deep copy Example of a deep copy

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

tuple

(1, 'hello', 3.5)

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

dict

{'a': 1, 'b': 2}

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

set

{'a', 'b', 'c'}

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#

  • Loopsfor, while, break/continue/else

  • range() — 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

Table 1 Data Structure Comparison#

Type

Mutable

Ordered

Duplicates

list

Yes

Yes

Yes

tuple

No

Yes

Yes

dict

Yes

Yes

Keys: No

set

Yes

No

No

Note

Reminder: Review and experiment with all provided code before next class.

Preview: What’s Next in L4#

📖 L4: Functions — Part I
  • 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.