8.2 — Shallow vs. deep copy, Handling exceptions

with statement to open files

1with open("myfile.txt", "r") as fobj:
2 # file remains open here inside with-statement
3 file_content = fobj.read()
4
5# the file is automatically closed when
6# the with-statement is done
7
8print(file_content)

Any valid operations/methods can be performed on the file object inside the with statement.

1with open("myfile.txt", "r") as fobj:
2 for line in fobj:
3 line = line.rstrip("\n")
4 print(line)

For the Write and Append modes, the write method can be used in the with statement.

1with open("myfile.txt", "r") as fobj:
2 print("hello")
3
4print(fobj.read())
Output
hello
Traceback (most recent call last):
  File "lecture_8.py", line 4, in <module>
    print(fobj.read())
ValueError: I/O operation on closed file.

Reading & writing files using pathlib

To read a file entirely into a string:

1from pathlib import Path
2
3file_content = Path("myfile.txt").read_text()
4print(file_content)

To write text into a file:

1from pathlib import Path
2
3data = """Name,A1,A2,A3
4Student-A,90,80,100
5Student-B,100,90,100
6"""
7
8# this will delete existing contents of the file
9Path("grades.txt").write_text(data)

Shallow copy

When we create a new list using slicing or list() function on a nested list, inner lists are not copied but they are shared. Such a copy is called shallow copy.

1nested_list = [[3, 17, 42], [10, 20, 30]]
2
3new_copy = nested_list[:] # shallow copy
4
5new_copy = list(nested_list) # shallow copy

nested_list = [[3, 17, 42],
[10, 20, 30]]
new_copy = nested_list[:]

Changing values in a shallow copy affects the original nested list because inner lists are shared.

1nested_list = [[3, 17, 42],
2 [10, 20, 30]]
3
4new_copy = nested_list[:]
5# new_copy = list(nested_list)
6
7new_copy[0][2] = 123
8
9print(new_copy)
10print(nested_list)
Output
[[3, 17, 123], [10, 20, 30]]
[[3, 17, 123], [10, 20, 30]]

Similary dict(), tuple(), set() functions will make a shallow copy.

student = {"name": "Reza", "ID": 2601000, "grades": [95, 95, 97]}
student_copy = dict(student)

Changing values via the shallow copy affects the original dictionary because grades list is shared.

1student = {"name": "Reza", "ID": 2601000, "grades": [95, 95, 97]}
2
3student_copy = dict(student)
4student_copy["grades"][0] = 100
5
6print(student_copy)
7print(student)
Output
{'name': 'Reza', 'ID': 2601000, 'grades': [100, 95, 97]}
{'name': 'Reza', 'ID': 2601000, 'grades': [100, 95, 97]}

Deep copy

deepcopy function from copy module can copy a nested structure recursively (all inner lists etc. are copied as well). Such a copy is called deep copy.

1import copy
2
3nested_list = [[3, 17, 42],
4 [10, 20, 30]]
5new_copy = copy.deepcopy(nested_list)
6new_copy[0][2] = 123
7
8print(new_copy)
9print(nested_list)
Output
[[3, 17, 123], [10, 20, 30]]
[[3, 17, 42], [10, 20, 30]]

1import copy
2
3student = {"name": "Reza", "ID": 2601000, "grades": [95, 95, 97]}
4
5student_copy = copy.deepcopy(student)
6student_copy["grades"][0] = 100
7
8print(student_copy)
9print(student)
Output
{'name': 'Reza', 'ID': 2601000, 'grades': [100, 95, 97]}
{'name': 'Reza', 'ID': 2601000, 'grades': [95, 95, 97]}

Time for problems about file reading/writing on Ed Lessons.

Types of Errors

  • Syntax Errors: When syntax is incorrect such as wrong punctuations, invalid characters, indentation etc.
    • Program does not even run in this case.
  • Runtime Errors, also called Exceptions, occur when there is a problem in the program during execution.
    • All code executes until an exception occurs.
  • Semantic or Logic errors are said to occur when a program executes without a problem but does not produce correct output as expected.

In Python, all exceptions are objects of some exception type.
Common exceptions are:

Exception TypeMeaning
IndexErrorIndex is out of range in a list or tuple
KeyErrorSpecified key does not appear in a dictionary
NameErrorSpecified local or global name does not exist
TypeErrorOperation or function applied to an inappropriate type
ValueErrorOperation or function applied to correct type but inappropriate value
ZeroDivisionErrorSecond operand of division or remainder operation is zero

Exception Traceback

Traceback is an error message that allows tracing an exception back to its origin

1def func():
2 return 5 / 0
3
4def main():
5 return func()
6
7main()
Output
Traceback (most recent call last):
  File "myprogram.py", line 7, in <module>
    main()
  File "myprogram.py", line 5, in main
    return func()
  File "myprogram.py", line 2, in func
    return 5 / 0
ZeroDivisionError: division by zero

Using try statement to handle errors

1try:
2 # try-block: code that may cause runtime error
3except:
4 # except-block: handle the error here
  • Identify a code that can potentially produce errors
  • Put that code in try-block.
  • Write code in except-block to handle the case when error occurs

Example

Suppose we want to take a number from user input:

1number = float(input('Please enter a number: '))
Output
ValueError: could not convert string to float: 'abcd'

Since we know that float() function throws the ValueError, we can handle that error using try statement.

1try:
2 number = float(input('Please enter a number: '))
3 # the following line only executes when float() function
4 # worked i.e. it did not throw ValueError
5 print("You entered: ", number)
6except ValueError:
7 print("Please enter valid number!")

There are two cases when we run the code above:

No error occurs in try-block
except-block is not executed.

Output
Please enter a number: -3.1415
You entered:  -3.1415

Error occurs in try-block
except-block is executed.

Output
Please enter a number: abcd
Please enter valid number!

Using try statement in a loop, ask user to input a number (float) until they enter a valid number.
You can use a break statement in the try-block.

Output
Please enter a number: abcd
Invalid number!
Please enter a number: -1.61
Correct number entered: -1.61

1while True: # Loop forever
2 try:
3 number = float(input('Please enter a number: '))
4 break # Get out of the loop
5 except ValueError:
6 print('Invalid number!')
7
8print("Correct number entered:", number)

Unhandled exceptions are thrown as usual

If an exception occurs in try-block but it is not the same type as in the except part, the exception occurs as usual i.e. except-block is not executed and program crashes.

1student_grades = {"Reza": 90.0}
2
3try:
4 name = input('Enter name: ')
5 grade = float(input("Enter grade to add: "))
6 student_grades[name] += grade
7 print(student_grades)
8except ValueError:
9 print('Grade should be a number!')
Output
Enter name: Dev
Enter grade to add: 5
Traceback (most recent call last):
  File "myprogram.py", line 20
    student_grades[name] += grade
KeyError: 'Dev'

Catching multiple exceptions

Except blocks can be chained to handle multiple exceptions that may occur in try-block.

Depending on the exception that occurs, only one of the except-blocks executes. Others are skipped.

1try:
2 # try-block: code that may cause runtime error(s)
3except Error1:
4 # handle the Error1 here
5except Error2:
6 # handle the Error2 here

1student_grades = {"Reza": 90.0}
2
3try:
4 name = input('Enter name: ')
5 grade = float(input("Enter grade to add: "))
6 student_grades[name] += grade
7 print(student_grades)
8except ValueError:
9 print('Grade should be a number!')
10except KeyError:
11 print(name, "was not found.")

No error

Output
Enter name: Reza
Enter grade to add: 5
{'Reza': 95.0}

ValueError

Output
Enter name: Reza
Enter grade to add: 10x
Grade should be a number!

KeyError

Output
Enter name: Dev
Enter grade to add: 5
Dev was not found.

We can have a default except block without any exception type to handle exception of any kind.

1student_grades = {"Reza": 90.0}
2
3try:
4 name = input('Enter name: ')
5 grade = float(input("Enter grade to add: "))
6 student_grades[name] += grade
7 print(no_such_variable) # error here
8except ValueError:
9 print('Grade should be a number!')
10except KeyError:
11 print(name, "was not found.")
12except:
13 print("Some error occured.")
Output
Enter name: Reza
Enter grade to add: 5
Some error occured.

(Again, only one of the except-blocks will execute.)

In general it is not a good practice to catch all errors using a default block.
Instead, specific errors should be handled explicitly by using an exception type.

finally block

  • We can have an optional finally block in a try statement; it is always executed whether an exception occurs in try block or not.
  • It is useful to clean up resources (e.g. closing a file), which needs to be done even when exceptions occur.
1try:
2 # try-block: code that may cause runtime error(s)
3except Error1:
4 # handle the Error1 here
5except Error2:
6 # handle the Error2 here
7finally:
8 # this block always executes

For this example, download the files read_matrix.py and matrixdata.txt from Ed Lesson and keep it in the same folder as the program.

When to use try statement

  • It is a bad practice to use a try/except to “hide” bugs in the program!
  • try/except should be used when we know that a specific error may occurs and there is no other way to handle it

Good practice

1try:
2 number = float(input("Enter a number: "))
3except ValueError:
4 print('Number is not valid!')

because there is no other better way to check if a string contains a valid number.

Not a good practice

1student_grades = {"Reza": 90.0}
2try:
3 name = input('Enter name: ')
4 student_grades[name] += 10
5except KeyError:
6 print(name, "was not found.")

because there is another way to do this (shown on right ->)

1student_grades = {"Reza": 90.0}
2
3name = input('Enter name: ')
4
5if name in student_grades:
6 student_grades[name] += 10
7else:
8 print(name, "was not found.")

Where do Exceptions come from?

raise statement is used to throw an exception from our code to tell Python that an unexpected case or error has occurred.

raise SomeException("Some message")

Check euclidean_distance function in distance.py and distance2.py.

An exception raised in this way must be handled using try statements, otherwise Python will stop execution with the error as usual.

Example

How exceptions are raised in Python modules?
Open the following link and search for raise: https://github.com/python/cpython/blob/main/Lib/random.py

Checking type of an object

1# isinstance(obj, class):
2# Return whether an object is an instance of a class
3
4x = 123
5print(isinstance(x, int)) # True
6print(isinstance(x, float)) # False
7
8x = "apple"
9print(isinstance(x, str)) # True
10
11x = [1, 5, 9]
12print(isinstance(x, list)) # True
13print(isinstance(x, tuple)) # False

1# isinstance(obj, tuple_of_classes):
2# A tuple of classes, e.g. isinstance(x, (A, B, ...)), may be given.
3# Equivalent to isinstance(x, A) or isinstance(x, B) or ...
4
5x = 3.14
6print(isinstance(x, (int, float))) # True
7
8x = [1, 5, 9]
9print(isinstance(x, (list, tuple))) # True
10
11x = (11, 51, 4)
12print(isinstance(x, (list, tuple))) # True

Time for some problems on Ed Lessons.