0% found this document useful (0 votes)
47 views77 pages

Module 2

Module-2 covers working with strings, tuples, and lists in Python, detailing string methods, immutability, indexing, slicing, and comparison. It explains how to manipulate strings and lists, including traversing, counting, and using built-in functions like find. The module emphasizes the differences between mutable and immutable types and provides examples for practical understanding.

Uploaded by

vikasm0427
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
47 views77 pages

Module 2

Module-2 covers working with strings, tuples, and lists in Python, detailing string methods, immutability, indexing, slicing, and comparison. It explains how to manipulate strings and lists, including traversing, counting, and using built-in functions like find. The module emphasizes the differences between mutable and immutable types and provides examples for practical understanding.

Uploaded by

vikasm0427
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Module-2

Strings: Working with strings as single things, working with the parts of a string, Length,
Traversal and the for loop, Slices, String comparison, Strings are immutable, the in and not in
operators, A find function, Looping and counting, Optional parameters, The built-in find method,
The split method, Cleaning up your strings, The string format method.

Tuples: Tuples are used for grouping data, Tuple assignment, Tuples as return values,
Composability of Data Structures.

Lists: List values, accessing elements, List length, List membership, List operations, List
slices, Lists are mutable, List deletion, Objects and references, Aliasing, cloning lists, Lists and
for loops, List parameters, List methods, Pure functions and modifiers, Functions that produce
lists, Strings and lists, list and range, Nested lists, Matrices.

Strings

Working with strings as single things


In Python, a string is an object, just like a turtle instance.

Each string object has its own attributes and methods.

Example with turtle:


>> tess.turn(90) # calling a turtle method

Similarly, with strings:

our_string = "hello"
print(our_string.upper()) # Output: HELLO

Common String Methods


upper() → returns a new string in uppercase.

"python".upper() # Output: "PYTHON"

lower() → returns a new string in lowercase.

"Python".lower() # Output: "python"


capitalize() → makes the first character uppercase.

"python".capitalize() # Output: "Python"


● swapcase() → swaps uppercase ↔ lowercase.

"PyThOn".swapcase() # Output: "pYtHoN"

Strings are immutable – the original string remains unchanged, and a new string is returned.

🔹 Exploring Methods in Editors

● Strings have around 70 methods.

● To explore:

○ Use Help documentation.

○ In an editor (Spyder, PyScripter, Jupyter):

■ Type our_string. and press Tab → list of methods appears.

■ Typing a method shows parameters, return type, and docstring.

■ In Jupyter, press Shift+Tab after a method name for help.

Working with the Parts of a String


● Strings can be broken down into smaller parts using indexing.

● Python uses square brackets [ ] to select a character from a string.

🔹 Indexing Example
fruit = "banana"
letter = fruit[1]
print(letter) # Output: a
● Indexing starts from 0 (zero-based indexing).

○ fruit[0] → 'b'

○ fruit[1] → 'a'

● If we want the zero-th letter, we use index 0.

fruit = "banana"
print(fruit[0]) # Output: b

🔹 Concept of Index

● The value inside the brackets is called an index.

● Index specifies a position in an ordered collection (here, characters of the string).

● An index can be any integer expression.

🔹 Visualizing Indices

Using enumerate() to see characters with their indices:

fruit = "banana"
for i, ch in enumerate(fruit):
print(i, ch)

Output:

0 b
1 a
2 n
3 a
4 n
5 a
● Indexing returns a string of length 1 (Python does not have a separate character type).
The same indexing notation also works for lists.

Example with list:

numbers = [10, 20, 30, 40]


print(numbers[2]) # Output: 30

>>> prime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]

>>> prime_numbers[4] 11

>>> friends = ["Joe", "Zoe", "Brad", "Angelina", "Zuki", "Thandi", "Paris"]

>>> friends[3] 'Angelina'

Indexing lets us access individual characters (or elements in a list). In Python, both strings
and lists use the same square bracket notation for indexing.

Length
● The len() function returns the number of characters in a string.

🔹 Example – Finding Length


word = "banana"
print(len(word)) # Output: 6

🔹 Accessing the Last Character

● Indexing starts at 0, so the last character is at len(word) - 1.

word = "banana"
last = word[len(word) - 1]
print(last) # Output: a

Trying word[len(word)] will cause an error:

IndexError: string index out of range


🔹 Using Negative Indices

● Python allows negative indexing:

○ word[-1] → last character

○ word[-2] → second last character

○ and so on.

word = "banana"
print(word[-1]) # Output: a
print(word[-2]) # Output: n

🔹 Works with Lists Too

● Negative indexing also works for lists:

numbers = [10, 20, 30, 40]


print(numbers[-1]) # Output: 40
print(numbers[-2]) # Output: 30

● len(string) gives the length.

● Last element can be accessed by len(string)-1 or by using negative indices.

1. Traversing a String with a while loop (inefficient way)

Even though the original code isn’t shown, this is what it's describing:

fruit = "banana"
ix = 0
while ix < len(fruit):
print(fruit[ix])
ix += 1
● This prints each character in "banana" on a new line.

● It uses indexing (fruit[ix]) and manually updates the index.

● It works, but it’s longer and harder to read.

2. Traversing a String with a for loop (better way)

Here's a cleaner version using a for loop:

fruit = "banana"
for c in fruit:
print(c)

● Here, c takes each character in "banana" one at a time.

● It’s shorter, cleaner, and more Pythonic.

3. Abecedarian Example with Concatenation

This is a creative use of a for loop and string concatenation:

prefixes = "JKLMNOPQ"
suffix = "ack"

for p in prefixes:
print(p + suffix)

Output:

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

Slices
A substring of a string is obtained by taking a slice. Similarly, we can slice a list to refer to
some sublist of the items in the list.

The slice syntax is:

sequence[n:m]

This returns the part of the string (or list) from the n-th character to the m-th, including the first
but excluding the last.

This behavior makes sense if you imagine the indices pointing between the characters, like this:

String: A B C D E
Index: 0 1 2 3 4 5
Slice A[1:4] → characters at indices 1, 2, 3 → 'BCD'

● sequence[n:m]: from index n to m-1.

● If you omit n, the slice starts at the beginning.

● If you omit m, it goes to the end.

● If n is greater than the length, no error occurs—just an empty result.

● Length of the slice = m - n.

Examples
1. phrase[:] — Whole String

This slice copies the entire string.

Example:
phrase = "Python Programming"
print(phrase[:]) # Output: Python Programming

2. friends[4:] — From Index 4 to End

Returns a sublist (or substring) starting from index 4 up to the end.

Example (List):
friends = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank"]
print(friends[4:]) # Output: ['Eve', 'Frank']

Example (String):
friends = "ABCDEFG"
print(friends[4:]) # Output: EFG

3. phrase[-5:-3] — Negative Indices

Negative indices count from the end of the string.

● -1 = last character

● -2 = second last

● -5 = fifth from the end

● -3 = third from the end

Example:
phrase = "hello world"
print(phrase[-5:-3]) # Output: wo
Explanation:

● phrase[-5] → 'w'

● phrase[-4] → 'o'

● Excludes phrase[-3] ('r')

String Comparison
The comparison operators (like ==, <, >, !=, etc.) work on strings, just like they do with
numbers. You can use them to check if strings are equal or to compare their lexicographical
order (similar to alphabetical order).

Equality Comparison

To check whether two strings are equal, you can use the == operator:

Example:

word1 = "apple"

word2 = "apple"

print(word1 == word2) # Output: True

You can also use != to check if two strings are not equal:

word1 = "apple"

word2 = "orange"

print(word1 != word2) # Output: True


Lexicographical Comparison

You can use <, >, <=, >= to compare strings in dictionary (lexicographical) order.

Example:

print("apple" < "banana") # Output: True

print("apple" > "banana") # Output: False

print("Apple" < "banana") # Output: True

print("Zebra" < "apple") # Output: True

print("apple" < "Apple") # Output: False

● Strings are compared character by character using their Unicode (ASCII) values.

● Uppercase letters come before lowercase letters in ASCII.

ASCII order:

"A" = 65

"Z" = 90

"a" = 97

"z" = 122

So:

print("Apple" < "apple") # True, because 'A' < 'a'

Strings are Immutable


In Python, strings are immutable. This means that once a string is created, its contents
cannot be changed.
Invalid Attempt to Modify a String

It may be tempting to try to change a character in a string using indexing and assignment, like
this:

Example:

greeting = "Hello, world!"

greeting[0] = "J" # ❌ Invalid

print(greeting)

Output:

TypeError: 'str' object does not support item assignment

This happens because strings are immutable, and you cannot modify them
directly.

Create a New String

Instead of modifying the original string, you can create a new string using concatenation and
slicing.

Example:

greeting = "Hello, world!"

new_greeting = "J" + greeting[1:]

print(new_greeting) # Output: Jello, world!

Explanation:

● greeting[1:] → "ello, world!"


● "J" + greeting[1:] → "Jello, world!"

This operation creates a new string based on the original but does not affect the
original string.

● Strings are immutable

● Lists, however, are mutable (you can change list elements using indexing)

The in and not in Operators


The in operator tests for membership. When both arguments are strings, it checks whether
the left argument is a substring of the right argument.

Using in with Strings

Example:

fruit = "banana"

print("nan" in fruit) # Output: True

Explanation:

● "nan" is a substring of "banana" → returns True.

Special Cases

A string is a substring of itself:

print("banana" in "banana") # True


The empty string is a substring of any string:

print("" in "apple") # True

Computer scientists pay special attention to these edge cases because they
behave differently than what people may intuitively expect.

The not in Operator

The not in operator returns the logical opposite of in.

Example:

fruit = "banana"

print("z" not in fruit) # Output: True

print("a" not in fruit) # Output: False

Removing Vowels Using in

We can use in along with string concatenation to remove all vowels from a string.

Example:

def remove_vowels(s):

vowels = "aeiou"

result = ""

for letter in s:

if letter.lower() not in vowels:

result += letter
return result

print(remove_vowels("Hello World")) # Output: Hll Wrld

Explanation:

● letter.lower() ensures both uppercase and lowercase vowels are removed.

● Without lower(), "E" in "Hello" would not be removed since "E" != "e".

A find function
Example:

def find(word, letter):

index = 0

while index < len(word):

if word[index] == letter:

return index

index += 1

return -1

● This function searches for the first occurrence of the letter letter in the string word.

● It uses a while loop to check each character in word one by one.

● If it finds the letter, it returns the index where it was found.

● If the letter is not found in the word, it returns -1.


Using Python’s built-in find() method

Python strings have a built-in method .find() that does the same job.

word = "banana"

print(word.find('a')) # Output: 1

print(word.find('z')) # Output: -1

● word.find('a') returns the index of the first 'a' (which is 1).

● If the character is not found, .find() returns -1, just like the function above.

Looping and counting

The following program counts the number of times the letter a appears in a string,
and is another example of the counter pattern introduced in Counting digits:

Program:

def count_a(text):

count = 0 # Initialize counter to 0

for letter in text: # Loop through each letter in the string

if letter == "a": # Check if the letter is 'a'

count += 1 # Increment counter by 1

return count # Return the total count

# Test the function

print(count_a("banana") == 3) # Output: True

Optional Parameters
Extending the find function with a start position:

To find the second or third occurrence of a character in a string, we can modify the find
function by adding a third parameter that indicates the starting position in the search string.

Example: find2 function with start parameter

def find2(word, letter, start):

index = start

while index < len(word):

if word[index] == letter:

return index

index += 1

return -1

How to use find2:

print(find2("banana", "a", 2)) # Output: 3

● The search starts at index 2.

● The function returns 3, which is the first occurrence of 'a' starting at index 2.

What about:

print(find2("banana", "n", 3)) # Output: 4

● Searching for 'n' starting at index 3.


● Returns 4, which is the index of 'n' after position 3.

Combining find and find2 with an optional parameter

We can improve find by adding an optional parameter for start. If the caller doesn’t provide
start, it defaults to 0, meaning the search starts at the beginning.

def find(word, letter, start=0):

index = start

while index < len(word):

if word[index] == letter:

return index

index += 1

return -1

Usage examples:

print(find("banana", "a")) # Output: 1 (starts from 0)

print(find("banana", "a", 2)) # Output: 3 (starts from index 2)

Adding another optional parameter for end position:

We can further extend find to search within a slice of the string, using optional start and end
parameters:

def find(word, letter, start=0, end=None):


if end is None:

end = len(word)

index = start

while index < end:

if word[index] == letter:

return index

index += 1

return -1

start and end:

● start and end behave like the parameters in the range() function.

● The search includes the character at start but excludes the character at end.

● If end is omitted, the search goes to the end of the string.

The built-in find method

Built-in find method in Python strings

Python strings already have a powerful built-in find method that can do everything our
custom find function can do — and even more!

Features of the built-in find method

● Can find single characters as well as substrings.

● Accepts optional start and end parameters to specify the search range.
● Returns the lowest index where the substring is found.

● Returns -1 if the substring is not found.

Examples:

text = "banana"

# Find single character

print(text.find("a")) # Output: 1

# Find substring

print(text.find("nan")) # Output: 2

# Find with start parameter

print(text.find("a", 2)) # Output: 3

# Find with start and end parameters

print(text.find("a", 2, 4)) # Output: 3

# Substring not found

print(text.find("z")) # Output: -1
Why use built-in methods?

● Built-in methods are optimized and tested.


● They reduce the need to reinvent the wheel.
● Help you write concise and efficient code.

The split method

What does the split method do?

The split method is one of the most useful string methods. It splits a single multi-word
string into a list of individual words.

● It removes all whitespace (spaces, tabs, newlines) between words.

● This is especially helpful for processing text input where words are separated by spaces
or other whitespace characters.

How to use split

text = "Hello world this is Python"

words = text.split()

print(words)

Output:

['Hello', 'world', 'this', 'is', 'Python']

● The string text is split at every whitespace.

● The result is a list of words.

Why use split?


● To easily work with individual words in a string.

● To convert a line of input into manageable pieces.

● Helpful in text processing, parsing, and data cleaning.

● If you provide an argument to split(separator), it splits based on that separator


instead of whitespace.

Example:

csv = "apple,banana,orange"

fruits = csv.split(",")

print(fruits)

Output:

['apple', 'banana', 'orange']

Cleaning up your strings

Why clean strings?

When working with strings, especially those read from files or the
internet, they often contain punctuation, tabs, newlines, or other
unwanted characters.

● For tasks like counting word frequencies or spell checking, it's


important to remove punctuation and clean the text.

● Since strings are immutable (cannot be changed), we create a new


string by traversing the original and omitting unwanted
characters.
Removing punctuation manually

def remove_punct(text):

result = ""

for char in text:

if char not in ".,!?":

result += char

return result

print(remove_punct("Hello, world!"))

Output:

Hello world

Using Python’s string module for punctuation

Setting up punctuation manually is error-prone. Python’s string module


provides a constant string.punctuation containing all punctuation
characters.

import string

def remove_punct(text):

result = ""

for char in text:


if char not in string.punctuation:

result += char

return result

print(remove_punct("Hello, world!"))

Output:

Hello world

Combining remove_punct and split

Cleaning punctuation and splitting into words is a powerful


combination.

import string

def clean_and_split(text):

no_punct = ""

for char in text:

if char not in string.punctuation:

no_punct += char

return no_punct.split()
sample = '"Well, I never did!", said Alice. "Are you very, very,
sure?"'

words = clean_and_split(sample)

print(words)

Output:

['Well', 'I', 'never', 'did', 'said', 'Alice', 'Are', 'you',


'very', 'very', 'sure']

● The punctuation is removed.

● The split() method splits the string into a list of words,


handling whitespace including tabs and newlines.

The string format method

Introduction to the format method

The easiest and most powerful way to format strings in Python 3 is to


use the format method.

● It uses placeholders in a template string, like {0}, {1}, {2},


etc.

● The format method substitutes its arguments into these


placeholders.

● The numbers inside the curly braces correspond to the index of


the argument passed to format.

Basic examples

print("My name is {0} and I am {1} years old.".format("Alice", 30))


print("The numbers are {0}, {1}, and {2}.".format(5, 10, 15))

print("{0} {1} {0}".format("Hello", "World"))

Output:

My name is Alice and I am 30 years old.

The numbers are 5, 10, and 15.

Hello World Hello

● You can reuse the same argument multiple times by referencing its
index.

● If you refer to an index not provided, you get an IndexError.

Format specifications

Each placeholder can also include a format specification introduced by


a colon ::

● Alignment:

○ < : left-align

○ ^ : center-align

○ > : right-align

● Width: Specify minimum width (e.g., 10).

● Type conversions:
○ f : floating point

○ .2f : floating point with 2 decimal places

○ x : integer to hexadecimal

Formatting example with specifications

print("{0:<10} | {1:^10} | {2:>10}".format("left", "center", "right"))

print("Pi to 2 decimals: {0:.2f}".format(3.14159))

print("Hex of 255: {0:x}".format(255))

Output:

left | center | right

Pi to 2 decimals: 3.14

Hex of 255: ff

Using format for neat tables

Without formatting:

print("i\ti**2\ti**3\ti**5\ti**10\ti**20")

for i in range(1, 11):

print(i, "\t", i**2, "\t", i**3, "\t", i**5, "\t", i**10, "\t",
i**20)
Output can look misaligned because tab stops vary depending on number
length.

Improved version with formatting

print("{0:>2} {1:>8} {2:>10} {3:>12} {4:>15} {5:>20}".format(

"i", "i**2", "i**3", "i**5", "i**10", "i**20"))

for i in range(1, 11):

print("{0:2} {1:8} {2:10} {3:12} {4:15} {5:20}".format(

i, i**2, i**3, i**5, i**10, i**20))

Output:

i i**2 i**3 i**5 i**10


i**20

1 1 1 1 1
1

2 4 8 32 1024
1048576

3 9 27 243 59049 3486784401

4 16 64 1024 1048576 1099511627776

5 25 125 3125 9765625 95367431640625

6 36 216 7776 60466176 3656158440062976

7 49 343 16807 282475249 79792266297612001

8 64 512 32768 1073741824 1152921504606846976

9 81 729 59049 3486784401 12157665459056928801


10 100 1000 100000 10000000000 100000000000000000000

● Columns are right-justified and neatly aligned.Formatting makes


output clearer and more readable regardless of number size.

Tuples

Tuples are used for grouping data

● A tuple is a way to group multiple values into a single compound


value.

● It is a comma-separated sequence of values, usually enclosed in


parentheses for clarity.

● Example of a tuple grouping two values:

pair = (3, 4)

print(pair) # Output: (3, 4)

● Parentheses help disambiguate nested tuples and are used to


create an empty tuple:

empty_tuple = ()

● Tuples are useful for representing records (or structs) — related


pieces of data grouped together:

student = ("Alice", 21, "Computer Science")

● Tuples support sequence operations like indexing:


print(student[0]) # Output: Alice

print(student[1]) # Output: 21

● Tuples are immutable — trying to change an element raises an


error:

student[1] = 22 # Raises TypeError: 'tuple' object does not


support item assignment

● You can create a new tuple by slicing and concatenating parts of


an old tuple:

julia = ("Julia", "Roberts", 50)

julia_new = julia[:2] + (51,)

print(julia_new) # Output: ('Julia', 'Roberts', 51)

● To create a single-element tuple, include a trailing comma:

single_element = (5,)

print(type(single_element)) # Output: <class 'tuple'>

not_a_tuple = (5)

print(type(not_a_tuple)) # Output: <class 'int'>

Tuple assignment

What is Tuple Assignment?


Python supports a powerful feature called tuple assignment, where you
can assign a tuple of variables on the left side of an assignment to a
tuple of values on the right side — all in one line.

● This is equivalent to doing multiple assignments separately.

● Important: The number of variables on the left must match the


number of elements on the right.

Tuple packing and unpacking

● Tuple packing: grouping values into a tuple automatically.

packed = 1, 2, 3 # This creates a tuple (1, 2, 3)

print(packed) # Output: (1, 2, 3)

● Tuple unpacking: extracting values from a tuple into variables.

bob = ("Bob", 19, "CS")

(name, age, studies) = bob

print(name) # Output: Bob

print(age) # Output: 19

print(studies) # Output: CS

Using tuple assignment to swap values


Traditionally, swapping values between two variables requires a
temporary variable:

a = 5

b = 10

temp = a

a = b

b = temp

print(a, b) # Output: 10 5

With tuple assignment, swapping is cleaner and easier:

a = 5

b = 10

a, b = b, a

print(a, b) # Output: 10 5

● Here, (b, a) is a tuple packed on the right side.

● On the left side, a, b is unpacked to assign the values


respectively.

.All expressions on the right side are evaluated before


any assignments happen.
.The number of variables on the left and the number of values on the
right must be the same, otherwise Python raises a ValueError.

Tuples as return values

Returning multiple values with tuples

● A function in Python can only return one value.

● By returning a tuple, a function can effectively return multiple


values grouped together.

● This is very useful when you want to return related data, for
example:

○ A batsman’s highest and lowest scores.

○ The mean and standard deviation of a dataset.

○ The year, month, and day from a date.

○ The number of rabbits and wolves in ecological modeling.

Example: Return area and circumference of a circle

def circle_metrics(radius):

area = 3.14159 * radius ** 2

circumference = 2 * 3.14159 * radius

return (area, circumference)

result = circle_metrics(5)
print(result) # Output: (78.53975, 31.4159)

● The function circle_metrics returns a tuple containing area and


circumference.

● We get both results grouped together as a single return value.

Using tuple unpacking with return values

You can also unpack the returned tuple directly into separate
variables:

area, circumference = circle_metrics(5)

print("Area:", area) # Output: Area: 78.53975

print("Circumference:", circumference) # Output: Circumference:


31.4159

Composability of Data Structures

Combining data structures

● Data structures in Python can be nested and combined.

● For example, a list of pairs (tuples) can be created, where each


tuple itself may contain a list or other tuples.

● This allows for creating complex, structured data by composing


simpler elements.

Example: List of tuples with nested lists and tuples


movie_stars = [

("Julia Roberts", (1967, 10, 28), ["Pretty Woman", "Erin


Brockovich"]),

("Denzel Washington", (1954, 12, 28), ["Training Day", "Fences"]),

print(movie_stars)

# Output: [

# ('Julia Roberts', (1967, 10, 28), ['Pretty Woman', 'Erin


Brockovich']),

# ('Denzel Washington', (1954, 12, 28), ['Training Day', 'Fences'])

# ]

Tuples can contain other tuples and lists

● In the above example:

○ Each tuple has five elements or fewer.

○ Some elements themselves are tuples (for date of birth).

○ Some elements are lists (for movies).

● This shows heterogeneous composition — elements can be of


different types.
Lists

● A list is an ordered collection of values.

● The values inside a list are called elements or items.

● Lists are similar to strings, which are ordered collections of


characters.

● Unlike strings, the elements of a list can be of any type.

● Lists, strings, and other collections that maintain order are


called sequences.

List values

Creating lists

● The simplest way to create a list is by enclosing elements in


square brackets [].

numbers = [1, 2, 3, 4]

words = ["apple", "banana", "cherry"]

● The elements in a list don’t need to be the same type.

mixed_list = ["hello", 3.14, 10, [1, 2, 3]]

A list inside another list is called a nested list.


Empty list

● A list with no elements is called an empty list, denoted by [].

Assigning and printing lists

vocabulary = ["apple", "cheese", "dog"]

numbers = [17, 123]

an_empty_list = []

print(vocabulary, numbers, an_empty_list)

# Output: ['apple', 'cheese', 'dog'] [17, 123] []

Accessing elements

● The syntax to access elements of a list is the same as accessing


characters of a string.

● Use the index operator [] with an integer index inside the


brackets.

● Indices start at 0 — the first element is at index 0, the second


at 1, and so on.

words = ["apple", "banana", "cherry"]

print(words[0]) # Output: apple

print(words[2]) # Output: cherry


● Any expression that evaluates to an integer can be used as an
index.

i = 1

print(words[i]) # Output: banana

● Trying to access or assign an element outside the list's range


causes a runtime error.

print(words[5]) # IndexError: list index out of range

List traversal using indices

fruits = ["apple", "banana", "cherry"]

for i in range(len(fruits)):

print(fruits[i])

● This loop uses the variable i as an index to access each element


in the list.

● This pattern is called list traversal.

Better list traversal (direct iteration over elements)

for fruit in fruits:

print(fruit)
● This version directly iterates over the elements of the list.

● It is clearer and more Pythonic since it avoids explicit


indexing.

List length
● The built-in function len returns the length of a list, which is
the number of elements it contains.

● When using integer indices to access list elements in a loop, it


is a good practice to use len(list) as the upper bound.

● This ensures that loops will work correctly for any list size,
without needing manual updates.

horsemen = ["War", "Famine", "Pestilence", "Death"]

for i in range(len(horsemen)):

print(horsemen[i])

● In this loop, the last iteration occurs when i is len(horsemen) -


1, which corresponds to the last element’s index.

Better approach without indices:

for horseman in horsemen:

print(horseman)

● This version is clearer and preferred over the indexed loop.


Note on nested lists:

● Even if a list contains another list as an element, the nested


list counts as one element of the outer list.

nested_list = ["apple", "banana", ["cherry", "date"], "elderberry"]

print(len(nested_list)) # Output: 4

List membership
● The operators in and not in are Boolean operators used to test
membership in a sequence.

● They work with strings, lists, and other sequence types.

● in returns True if the element is found in the sequence,


otherwise False.

● not in returns the opposite: True if the element is not found.

Example with lists:

Suppose we want to count the number of students doing Computer Science


in a list of student-course tuples.

students = [

("John", ["CompSci", "Physics"]),

("Vusi", ["Maths", "CompSci", "Stats"]),

("Jess", ["CompSci", "Accounting", "Economics", "Management"]),

("Sarah", ["InfSys", "Accounting", "Economics", "CommLaw"]),


("Zuki", ["Sociology", "Economics", "Law", "Stats", "Music"])

count = 0

for student in students:

name, courses = student

if "CompSci" in courses:

count += 1

print("Number of students doing Computer Science:", count)

● This example shows how using the in operator simplifies checking


for membership inside nested lists.

● The program counts all students enrolled in "CompSci" courses.

List operations
● The + operator is used to concatenate two lists, joining them
end-to-end to form a new list.

● The * operator is used to repeat a list a given number of times,


creating a new list with the repeated elements.

Examples:
# Concatenation example

list1 = [1, 2, 3]

list2 = [4, 5, 6]

combined = list1 + list2

print(combined) # Output: [1, 2, 3, 4, 5, 6]

# Repetition example 1

zeros = [0] * 4

print(zeros) # Output: [0, 0, 0, 0]

# Repetition example 2

numbers = [1, 2, 3] * 3

print(numbers) # Output: [1, 2, 3, 1, 2, 3, 1, 2, 3]

● The + operator creates a new list by concatenating the two lists.

● The * operator creates a new list by repeating the original list


the specified number of times.

List slices
● Just like strings, lists support slice operations that let us
work with sublists — portions of the list extracted using a range
of indices.
The syntax for slicing a list is:

sublist = my_list[start:end]

● where start is the index where the slice begins (inclusive), and
end is where it ends (exclusive).

● If start is omitted, the slice begins from the start of the list;
if end is omitted, the slice continues to the end of the list.

● Slices create new lists and do not modify the original list.

Examples:

numbers = [10, 20, 30, 40, 50, 60]

# Slice from index 1 to 4 (excluding index 4)

sublist = numbers[1:4]

print(sublist) # Output: [20, 30, 40]

# Slice from start to index 3 (excluding index 3)

start_slice = numbers[:3]

print(start_slice) # Output: [10, 20, 30]

# Slice from index 3 to the end

end_slice = numbers[3:]

print(end_slice) # Output: [40, 50, 60]


# Slice the entire list (makes a copy)

full_slice = numbers[:]

print(full_slice) # Output: [10, 20, 30, 40, 50, 60]

● Slices can also be used to assign new values to parts of the


list, replacing a sublist with new elements.

● Negative indices can be used in slices to count from the end of


the list.

Lists are mutable


● Unlike strings, lists are mutable, which means we can change
their elements after creation.

● Using the index operator [] on the left side of an assignment


lets us update an element in the list.

● This is called item assignment and it works for lists but not for
strings.

Examples of item assignment:

fruit = ["banana", "apple", "quince"]

# Change first element

fruit[0] = "pear"
# Change last element

fruit[-1] = "orange"

print(fruit) # Output: ['pear', 'apple', 'orange']

● Item assignment does NOT work with strings:

word = "hello"

# word[0] = "j" # This will raise a TypeError because strings are


immutable.

● But it works with lists:

numbers = [1, 2, 3, 4]

numbers[2] = 99

print(numbers) # Output: [1, 2, 99, 4]

Updating multiple elements with slice assignment:

● The slice operator [:] can be used to update a whole sublist at


once.

letters = ['a', 'b', 'c', 'd', 'e']

letters[1:4] = ['x', 'y', 'z']

print(letters) # Output: ['a', 'x', 'y', 'z', 'e']


Removing elements by assigning an empty list:

letters = ['a', 'b', 'c', 'd', 'e']

letters[1:3] = []

print(letters) # Output: ['a', 'd', 'e']

Adding elements by assigning to an empty slice:

numbers = [1, 4, 5]

numbers[1:1] = [2, 3] # Insert [2,3] before index 1

print(numbers) # Output: [1, 2, 3, 4, 5]

List deletion
● Using slices to delete elements from a list can sometimes be
confusing or error-prone.

● Python provides a clearer way to delete elements using the del


statement.

● The del statement removes an element at a specific index from the


list.

Example of deleting a single element with del:

letters = ['a', 'b', 'c', 'd', 'e']


del letters[2] # Deletes element at index 2 ('c')

print(letters) # Output: ['a', 'b', 'd', 'e']

● Note: Using del with an invalid index will cause a runtime error
(IndexError).

Deleting a sublist with del and slices:

● del can also remove multiple elements by specifying a slice.

numbers = [1, 2, 3, 4, 5, 6]

del numbers[1:4] # Deletes elements at indices 1, 2, 3 (up to but not


including index 4)

print(numbers) # Output: [1, 5, 6]

● The slice follows usual rules: it includes all elements from the
first index up to, but not including, the second index.

This approach is often preferred because it’s clearer and more


explicit about deleting elements than assigning empty slices.

Objects and references


● When we assign values to variables, the variables refer to
objects in memory.

Example:

a = "banana"
b = "banana"

● After these assignments, both a and b refer to string objects


with the value "banana".

● But do a and b refer to the same object or two different objects


that happen to have the same value?

There are two possibilities:

1. a and b refer to two different objects, each containing "banana".

2. a and b refer to the same string object in memory.

● To test whether two variables refer to the same object, Python


provides the is operator.

print(a is b) # True or False?

● In this case, it prints True, which means a and b do refer to the


same object.

● Python does this because strings are immutable. To optimize


memory, Python makes two variables with the same string value
point to the same object.
● This optimization does not apply to lists, which are mutable:

a = [1, 2, 3]

b = [1, 2, 3]

print(a == b) # True (values are equal)

print(a is b) # False (different objects)

● Here, a and b have the same values but refer to different list
objects.

● Immutable objects like strings with the same value can share the
same memory object.

● Mutable objects like lists with the same value generally reside
at different memory locations.

● Use is to test if two variables refer to the same object.

● Use == to test if two variables have equal values.

Aliasing
● Since variables refer to objects in memory, assigning one
variable to another makes both variables refer to the same
object.

Example:

a = [1, 2, 3]

b = a
● After this, both a and b point to the same list object.

● The state looks like this:

a ----> [1, 2, 3] <---- b

● Because the same list has two different names (a and b), this is
called aliasing.

● Changes made through one alias affect the other:

b[0] = 100

print(a) # Output: [100, 2, 3]

● Changing b changes a because they both refer to the same


underlying object.

● Aliasing can sometimes be useful, but often it is unexpected or


undesirable, especially when working with mutable objects like
lists.

● It is safer to avoid aliasing when working with mutable objects


to prevent accidental changes through aliases.

● For immutable objects (like strings and tuples), aliasing is not


problematic because you cannot change the object’s content.

● Python freely aliases immutable objects when it can optimize


memory, since changes are impossible.

● Aliasing happens when multiple variables refer to the same


object.
● Modifying a mutable object via one alias affects all aliases.

● Avoid aliasing with mutable objects to prevent bugs.

● Immutable objects (strings, tuples) are safe to alias because


they cannot be changed.

Cloning lists
● When we want to modify a list but keep a copy of the original
unchanged, we need to clone the list.

● Cloning means creating a new list with the same elements, rather
than copying the reference to the original list.

● The easiest way to clone a list is to use the slice operator:

a = [1, 2, 3]

b = a[:]

● Here, b = a[:] creates a new list that contains all elements from
a.

● The state now looks like this:

a ----> [1, 2, 3]

b ----> [1, 2, 3]

● a and b refer to different list objects even though they contain


the same values.
● Now we can modify b without affecting a:

b[0] = 100

print(a) # Output: [1, 2, 3]

print(b) # Output: [100, 2, 3]

● Changes to b do not change a, since they are separate lists.

● Use slicing (a[:]) to clone a list.

● Cloning avoids aliasing and lets you safely modify one list
without changing the other.

Lists and for loops


● The for loop works naturally with lists, allowing us to iterate
over each element easily.

● The generalized syntax of a for loop:

for variable in list_expression:

# do something with variable

● Example:

friends = ["Alice", "Bob", "Charlie"]

for friend in friends:


print(friend)

● This reads like English: For every friend in the list of friends,
print the friend.

● Any list expression can be used in a for loop. For example:

for number in range(20):

if number % 3 == 0:

print(number)

● Prints all multiples of 3 between 0 and 19.

for fruit in ["banana", "apple", "quince"]:

print("I like to eat " + fruit + "s!")

● Expresses enthusiasm for different fruits.

● Since lists are mutable, we can traverse a list and modify its
elements. For example, to square all numbers in a list:

xs = [1, 2, 3, 4]

for i in range(len(xs)):
xs[i] = xs[i] ** 2

print(xs) # Output: [1, 4, 9, 16]

● Here, range(len(xs)) generates indices from 0 to length of the


list - 1, allowing access to both the index and the value.

● Python provides a nicer way to access both index and value using
enumerate:

for i, x in enumerate(xs):

xs[i] = x ** 2

● This makes the code cleaner and more readable.

● To see how enumerate works:

for pair in enumerate(xs):

print(pair)

● Output shows pairs of (index, value), e.g., (0, 1), (1, 2), etc.
● Use for variable in list to loop through items.

● Use range(len(list)) to loop through indices when you need to


modify the list.

● Use enumerate to get both index and value neatly during


iteration.

List parameters
● When you pass a list as an argument to a function, Python passes
a reference to the original list, not a copy or clone.

● This means that the function parameter becomes an alias for the
same list object that the caller has.

● Both the caller and the function share the same underlying list.

Example:

def multiply_by_two(stuff_list):

for i in range(len(stuff_list)):

stuff_list[i] *= 2

things = [1, 2, 3]

multiply_by_two(things)

print(things)

Output:

[2, 4, 6]
Explanation:

● In the function, the parameter stuff_list and the variable things


outside the function both refer to the same list object.

● Any changes made to the list inside the function affect the list
outside as well.

● The state snapshot before changes looks like this, showing one
list object referenced by two names (things and stuff_list):

things -----

[1, 2, 3]

stuff_list -

● Since the list is shared by two references, modifying it inside


the function will be visible outside the function.

Important:

● If a function modifies the elements of a list parameter, the


caller will see those changes because there is only one list
object.

● To avoid this, you need to clone the list before passing it


(e.g., using slicing things[:]).
List methods
● Lists have built-in methods accessed using the dot operator (.).

● One of the most useful methods is append, which adds an item to


the end of the list.

Example: Using append

fruits = ["apple", "banana", "cherry"]

fruits.append("date")

print(fruits)

Output:

['apple', 'banana', 'cherry', 'date']

Other useful list methods include:

● extend(iterable)
Adds each element of the iterable to the end of the list.

● insert(index, element)
Inserts an element at the specified index, shifting other
elements right.

● remove(element)
Removes the first occurrence of the element from the list.
● pop([index])
Removes and returns the element at the given index (last element
if index not provided).

● index(element)
Returns the index of the first occurrence of the element.

● count(element)
Counts how many times the element appears in the list.

● sort()
Sorts the list in place.

● reverse()
Reverses the elements of the list in place.

Example: Using multiple methods

numbers = [5, 2, 9, 1]

numbers.append(7) # Add 7 at the end

numbers.insert(2, 10) # Insert 10 at index 2

numbers.remove(2) # Remove first occurrence of 2

popped = numbers.pop() # Remove and return last element

print(numbers) # Current list

print("Popped:", popped)

Output:

[5, 10, 9, 1]
Popped: 7

Notes:

● Experiment with these methods to understand their behavior.

● Read the official documentation to explore all list methods and


their options.

Pure Functions and Modifiers


Pure Functions

A pure function is a function that does not change (modify) the values
that are passed to it.

● It only works with the given parameters.

● It returns a new value instead of changing the original data.

● It does not create any side effects.

Example: double_stuff written as a pure function

def double_stuff(a_list):

new_list = []

for value in a_list:

new_list.append(2 * value)

return new_list
This function:

● Takes a list as input.

● Creates a new list where every element is doubled.

● Returns the new list without changing the original list.

Modifiers

A modifier is a function that changes the values that are passed to


it.

● When lists are passed, modifiers can change them directly.

● This is called a side effect.

Example: double_stuff written as a modifier

def double_stuff(a_list):

for i in range(len(a_list)):

a_list[i] = 2 * a_list[i]

This function:

● Takes a list as input.

● Modifies the list directly (changes the original list itself).

● Does not return a new list.


Important Rule

When we assign values in Python:

● First, the right-hand side is evaluated.

● Then, the result is assigned to the left-hand side variable.

So, even if we use the same variable for storing the result, it is
safe.

Example:

numbers = [1, 2, 3]

numbers = double_stuff(numbers)

print(numbers)

Here,

● The double_stuff function is called.

● The returned list is assigned back to numbers.

● This does not cause problems because the old value is first used,
then replaced.

Summary:

● Pure functions → Do not change the original input, return new


results.
● Modifiers → Change the original input (cause side effects).

● Always remember the assignment rule: evaluate right-hand side


first, then assign.

Functions that Produce Lists


Sometimes, we need to write functions that create and return a new
list.
There is a common pattern for this:

General Pattern

def some_function(some_parameters):

result = []

for item in sequence:

result.append(expression_using(item))

return result

● Start with an empty list (result = []).

● Use a loop to process each item.

● Append the processed result to the new list.

● Return the list at the end.

Example 1: Pure version of double_stuff

This uses the same pattern.


def double_stuff(a_list):

result = []

for value in a_list:

result.append(2 * value)

return result

Output Example:

numbers = [1, 2, 3]

print(double_stuff(numbers)) # [2, 4, 6]

print(numbers) # [1, 2, 3] (unchanged)

Here, a new list is created instead of modifying the original.

Example 2: List of Prime Numbers

Suppose we already have a function is_prime(x) that checks if a number


is prime.
We can now use the pattern to make a list of all prime numbers less
than n.

def prime_list(n):

result = []

for i in range(2, n):

if is_prime(i):

result.append(i)

return result
Output Example (assuming is_prime is defined):

print(prime_list(10)) # [2, 3, 5, 7]

Summary

● Functions that create lists usually follow the “start empty → loop
→ append → return” pattern.

● This ensures we do not modify existing lists, but instead return


a new one.

● Example uses: doubling numbers, collecting primes, filtering


values, etc.

Strings and Lists

split() Method

● The split() method breaks a string into a list of substrings.

● By default, it uses whitespace (space, tab, newline) as the


separator.

Example:

song = "The rain in Spain"

words = song.split()

print(words)

Output:
['The', 'rain', 'in', 'Spain']

Here, the string is split into words wherever whitespace occurs.

split() with a Delimiter

● You can provide a delimiter (a string) as an optional argument.

● The string is split wherever that delimiter occurs.

Example:

song = "The rain in Spain"

words = song.split("ai")

print(words)

Output:

['The r', 'n in Sp', 'n']

Notice: the delimiter "ai" is not included in the result.

join() Method

● The inverse of split().

● join() takes a list of strings and combines them into one string,
using the specified separator (glue) between elements.
Example:

words = ['The', 'rain', 'in', 'Spain']

joined = ' '.join(words)

print(joined)

Output:

The rain in Spain

Using Different Glue

● You can use an empty string, space, or even multi-character


strings as glue.

Examples:

words = ['The', 'rain', 'in', 'Spain']

print(''.join(words)) # No space

print('-'.join(words)) # Hyphen as glue

print('***'.join(words)) # Multi-character glue

Output:

TheraininSpain

The-rain-in-Spain
The***rain***in***Spain

So, split() → string → list


join() → list → string

List and Range

list() Function

● Python has a built-in type conversion function called list().

● It tries to convert whatever you give it into a list.

Example:

s = "hello"

print(list(s))

Output:

['h', 'e', 'l', 'l', 'o']

range() Function

● range() generates a sequence of numbers.

● But it does this lazily (on demand).

● It doesn’t create all the numbers at once. Instead, it produces


them only when needed.

This is very memory-efficient.


Example: Lazy range()

def f(n):

""" Find the first positive integer between 101 and less

than n that is divisible by 21

"""

for i in range(101, n):

if (i % 21 == 0):

return i

print(f(110) == 105)

print(f(1000000000) == 105)

Output:

True

True

● In the second test (n = 1000000000), Python doesn’t generate a


billion numbers.

● As soon as it finds 105 divisible by 21, it stops.

● This is possible because range() is lazy.

Forcing range() into a List


● Sometimes, you may want the actual list of numbers instead of a
lazy object.

● Wrapping range() with list() forces Python to generate all the


elements.

Example:

print(list(range(5)))

Output:

[0, 1, 2, 3, 4]

Note on Python Versions

● Python 3 → range is lazy (memory efficient).

● Python 2 → range eagerly creates a list (memory heavy).

Looping and Lists

Loops in Computation

● Computers are powerful because they can repeat computations very


quickly and accurately.

● Loops are a central feature in almost all programs.

Lists in Loops
● Lists are useful when you need to keep data for later
computation.

● But if you don’t need to keep the data, it is better not to


generate a list, because it may waste memory.

Example: Two Versions of Summing Random Numbers

Version 1 (uses a list):

import random

def sum1():

nums = [random.random() for i in range(10000000)]

return sum(nums)

Version 2 (no list, uses generator):

import random

def sum2():

return sum(random.random() for i in range(10000000))

Both versions work correctly.

Why Prefer Version 2?


● Memory Efficiency:

○ sum1() creates a list of 10 million numbers in memory.

○ sum2() generates numbers one by one, without storing them.

● Performance:

○ sum2() uses less memory, so it is faster and avoids memory


errors.

● If you try very large inputs in sum1(), your computer might run
out of memory and crash.

Similar Example: Working with Files

● You often have two choices when reading files:

○ Whole file at once → read the entire file into a single


string.

○ Line-at-a-time → read and process each line one by one.

● Line-at-a-time processing is:

○ Safer

○ Works even with very large files

○ Was essential when old computers had very little memory.

● Whole file at once can be more convenient, but not always


efficient.
Loops repeat computations.
Lists store data for later use.
Avoid unnecessary lists → use generators when possible.
Line-by-line file reading is safer than reading all at once.

Nested Lists

What is a Nested List?

● A nested list is a list that contains another list as one of its


elements.

● Example:

nested = ["hello", 2.0, 5, [10, 20]]

Here, the element at index 3 is itself a list: [10, 20].

Accessing a Nested List

If we output the element at index 3:

print(nested[3])

Output:

[10, 20]

Extracting an Element from a Nested List

We can get elements step by step.


Step 1: Access the nested list.

inner_list = nested[3]

print(inner_list)

Output:

[10, 20]

Step 2: Access an element inside it.

print(inner_list[1])

Output:

20

Combining Steps

We can combine the two steps into one:

print(nested[3][1])

Output:

20

How It Works

● nested[3] → gives the 3rd element of nested (which is [10, 20]).

● nested[3][1] → from that inner list, it gives the 1st element (which
is 20).

● Bracket operators are evaluated from left to right.


A nested list is simply a list inside another list.
Use multiple brackets [ ] to drill down into deeper levels.

Matrices

Representing a Matrix with Nested Lists

● A matrix can be represented using nested lists.

● Example: The matrix

[123456789]\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9


\end{bmatrix}147258369

can be written in Python as:

mx = [

[1, 2, 3],

[4, 5, 6],

[7, 8, 9]

Here, mx is a list with three elements, and each element is a row


(which is itself a list).

Selecting an Entire Row


We can use single indexing to pick a row:

print(mx[1])

Output:

[4, 5, 6]

Selecting a Single Element

We can use double indexing (row, then column):

print(mx[1][2])

Output:

Explanation:

● mx[1] → second row → [4, 5, 6]

● mx[1][2] → third element of that row → 6

Other Representations

● The above example uses a list of rows.

● We could also store the matrix as a list of columns.


● Later, we’ll see more advanced alternatives like using a
dictionary to represent matrices.

Nested lists are a simple way to represent matrices.


First index → row, Second index → column.
Can also store as list of columns or use dictionaries for advanced cases.

Important Questions:

1. What is string slicing? Give examples.

2. Write a Python program to reverse a string using slicing.

3. Explain string methods upper(), lower(), and isupper() with examples.

4. Write a Python program to remove vowels from a given string.

5. Compare strings and lists with examples.

6. Write a Python program to count the frequency of characters in a string.

7. What is a tuple? How is it different from a list?

8. Write a program to find the sum of elements in a tuple.

9. Explain list slicing with examples.

10. Write a Python program to remove odd numbers from a list.

11. Discuss list methods append(), insert(), and remove() with examples.

12. Write a program to merge two lists into a single list.

13. Explain aliasing in lists with an example.

14. Write a Python program to find the maximum and minimum in a list.

15. Demonstrate nested lists with an example.

16. Write a program to find common elements between two lists.

17. Explain tuple packing and unpacking with examples.


18. Write a Python program to create a matrix using nested lists.

19. Explain the in and not in operators with examples.

20. Write a program to split a string into a list of words.

You might also like