Lecture 3: Working with files, processing and implementation of exceptions
Files
The built-in method
is used to open files. The path to the file is passed to this method as an argument - for example, filename.txt
. The open
function returns a file object that can be used to write data to the file or read data from it.
file = open('filename.txt')
Files can be opened in different modes – for writing, reading, reading and writing, or appending. This is done using modes, which are also passed as the second argument to the open
function. For example, a
is for appending, w
is obviously for writing, r
is for reading, and r+
is a mode that allows both reading and writing. Similarly, a file can be opened in binary mode, meaning working with binary data – to do this, you add the letter b
to the mode.
text_modes = ['r', 'w', 'a', 'r+']
binary_modes = ['br', 'bw', 'ba', 'br+']
file = open('filename.txt', 'w')
To write to a file, we apply the write
method to the corresponding file object, passing it a string. The write
method returns the number of characters that were written (or the number of bytes in the case of a byte string).
Note that this method erases everything that was previously in the file!
written = file.write('The world is changed.\nI taste it in the water.\n')
print(written)
# Output: 47
After working with a file, it must be closed so that it doesn't remain open and consume memory.
Files are closed using the close()
method.
file = open('filename.txt', 'w')
written = file.write('The world is changed.\nI taste it in the water.\n')
file.close()
print(written)
As we already know, to open a file for both reading and writing simultaneously, we need to use r+
. We can read data from the file using the read
method, which by default reads as much as it can (if the file is too large, it may not fit in memory). By the way, you can specify the exact amount of data you want to read by passing the size
argument to the read
method.
file = open('filename.txt', 'r+')
file.write('Hello World!!')
content = file.read(12)
file.close()
print(content)
# Output: Hello World!
When the entire file has been read, the pointer indicating our current position in the file is at the very end. If you try to read the file again, nothing will be found. To read the file again, you need to use the seek
method to move the pointer back to the beginning of the file. You must pass at least one argument to this method – the position where you want to place the cursor.
To check which position you're currently at in the file, you can use the tell
method.
file = open('filename.txt', 'r+')
written = file.write('The world is changed.\nI taste it in the water.\n')
content = file.tell()
print(content)
# Output: 47
content = file.read()
print(content)
# Output: ''
file.seek(0)
content = file.tell()
print(content)
# Output: 0
file.close()
Suppose you only need to read a single line, not the entire file. For this, there is a special method called readline
.
file_write = open('filename.txt', 'w')
file_write.write('The world is changed.\nI taste it in the water.\n')
file_write.close()
file_read = open('filename.txt', 'r+')
line = file_read.readline()
file_read.close()
print(result)
# Output: The world is changed.
To split the entire file into lines at once and place them into a list, there is the readlines
method.
file_write = open('filename.txt', 'w')
file_write.write('The world is changed.\nI taste it in the water.\n')
file_write.close()
file_read = open('filename.txt', 'r')
result = file_read.readlines()
file_read.close()
print(result)
# Output: ['The world is changed.\n', 'I taste it in the water.\n']
By the way, if you close the file, calling the read()
function will result in an error – a closed file cannot be read, which is quite logical.
It is recommended to open files a bit differently – using a context manager, which automatically closes files. You can open a file with the with
statement, assign the file object to a variable, and work with the file within that context block. After exiting the block, the Python interpreter will close the file independently.
with open('filename.txt', 'w') as f:
f.write('The world is changed.\nI taste it in the water.\n')
with open('filename.txt', 'r') as f:
print(f.read())
# Output: The world is changed.
# I taste it in the water.
Exception classes and their handling
I assume you may have already heard about exceptions. To begin working with them, let's try raising an exception and see what happens. For demonstration purposes, we'll use the interactive mode.
>>> 1/0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
When dividing by 0, an exception is expectedly raised, and in this case, information about its type (in our case, ZeroDivisionError
), additional details about the exception and the call stack are printed to the standard output.
In this example, the call stack is quite small. However, when an exception occurs in a real program, the call stack can show the entire sequence of function calls that led to the exception.
In Python, there are two major types of exceptions. The first is exceptions from the standard Python library, and the second type is custom exceptions. These can be generated and handled by the programmer when writing Python programs. Let's take a look at the hierarchy of commonly encountered exceptions in Python's standard library. All exceptions inherit from the base class BaseException
.
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- AssertionError
+-- AttributeError
+-- LookupError
+-- IndexError
+-- KeyError
+-- OSError
+-- SystemError
+-- TYpeError
+-- ValueError
There are several system exceptions, such as SystemExit
(raised if we call the os.exit()
function), KeyboardInterrupt
(raised when we press the Ctrl + C key combination), and so on. All other exceptions are derived from the base class Exception
. It is from this class that you should derive your own exceptions.
Now, let's take a look at and discuss some exceptions from the standard library, such as AttributeError
, IndexError
, KeyError
, TypeError
, and try to raise them.
The AttributeError
exception is raised when there is an attempt to access a non-existent attribute of a class.
>>> class MyClass():
... pass
...
>>> obj = MyClass()
>>> obj.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'foo'
When trying to access a value using a non-existent key in a dictionary, a KeyError
is raised.
>>> d = {"foo": 1}
>>> d["bar"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'bar'
When attempting to access a non-existent index in a list, an IndexError
is raised.
>>> l = [1, 2]
>>> l[10]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
If you try to convert a string consisting of letters to an integer, you will get a ValueError
.
>>> int("asdf")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'asdf'
You can get a `TypeError` when trying to add an integer to a string.
>>> 1 + "10"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Exception Handling (try-except construct)
If an exception is raised, the Python interpreter will stop its execution, and the call stack and the exception type information will be displayed. Exceptions can be handled using a try-except block to prevent the program from stopping. This means that the code that might potentially raise exceptions is enclosed in a try block, and if an exception is raised, control will be passed to the except block. This way, you can catch all exceptions generated in the try block.
try:
1 / 0
except Exception:
print("Error")
In the `except` block, you can specify the type of exception (in this case, `Exception`) to catch exceptions of all types whose class is a parent of that type. Generally, it is not advisable to catch all exceptions, as this can lead to unexpected behavior in your program. Let's demonstrate this with an example.
while True:
try:
number = int(input('Enter a number: '))
break
except:
print('Incorrect value!')
If we run this program in the command line and try to terminate it with the Ctrl+C combination, we will get the response "Invalid value," which is different from the desired behavior of the program.
Therefore, it is always important to handle specific exceptions. Let's rewrite this program according to this rule.
while True:
try:
number = int(input('Enter a number: '))
break
except ValueError:
print('Incorrect value')
The `try-except` construct can also have an `else` block. The `else` block is executed if no exceptions are raised in the `try` block.
while True:
try:
number = int(input("Enter a number: "))
except ValueError:
print("Incorrect value")
else:
break
If you need to handle multiple exceptions of different classes, you can use several `except` blocks and specify different exception classes in each one.
while True:
try:
number = int(input("Enter a number: "))
break
except ValueError:
print('Incorrect value')
except KeyboardInterrupt:
print('Exit')
break
If the exception handler is the same for multiple exceptions, you can pass several exceptions as a list in the except
block.
total_count = 100_000
while True:
try:
number = int(input('Enter a number: '))
total_count = total_count / number
break
except (ValueError, ZeroDivisionError):
print('Incorrect value')
It can be convenient to use the hierarchy of exception classes. Let's look at two exception classes, IndexError
and KeyError
, and their parent class LookupError
.
#+-- LookupError
# +-- IndexError
# +-- KeyError
>>> issubclass(KeyError, LookupError)
True
>>> issubclass(IndexError, LookupError)
True
Thanks to inheritance, we can handle both errors at once in the following program, which retrieves text from T-shirts stored in a database, where they are sorted by color.
database = {
"red": ["fox", "flower"],
"green": ["peace", "M", "python"]
}
color = input('Enter color: ')
number = input('Enter number: ')
try:
label = database[color][int(number)]
print(f'You chose: {label}')
except LookupError: # instead of (IndexError, KeyError)
print('Object not found')
The `finally` block in the `try-except` construct is designed for handling cleanup tasks. Let's consider a problem: for example, we open a file, read its lines, and process them. If an unexpected exception occurs during the program's execution, the file might not be closed properly.
Leaving open file descriptors can lead to resource exhaustion, which should be avoided. Similarly, open sockets may accumulate or memory may not be released. There are two options to control such situations: context managers or the `finally` block in exceptions.
In this example, we wrote a `finally` block where we call the `close()` method for the file object `file`. Whether an exception occurs, the `finally` block will always be executed, ensuring that the file is closed.
try:
file = open('/etc/hosts')
for line in file:
print(line.rstrip('\n'))
1 / 0
except Exception:
print('Error')
finally:
file.close()
To access the exception object, you need to use the construct except ... as ...
. In the following example, if an OSError
is raised, the exception object will be associated with the variable err
, and this variable will be accessible in the except
block.
Each exception object has its own properties, such as errno
and strerror
, which represents the error code and the string description of the error. Using these attributes, you can access and handle exceptions appropriately.
try:
with open('/file/not/found') as file:
content = file.read()
except OSError as err:
print(err.errno, err.strerror)
# Output: 2 No such file or directory
It is also important to mention that you can print the exception itself. This is commonly done, as such a message usually provides a quick understanding of what went wrong.
try:
with open('/file/not/found') as file:
content = file.read()
except OSError as err:
print(err)
# Output: [Errno 2] No such file or directory: '/file/not/found'
Raising exceptions
It's quite simple: to raise an exception, you need to use the `raise` keyword, followed by the exception class.
raise TypeError
"""
Output:
Traceback (most recent call last):
File "test.py", line 1, in <module>
raise TypeError
TypeError
"""
Optionally, you can also pass an error message when raising an exception. This message provides more context about the error.
raise ValueError('Message here')
"""
Output:
Traceback (most recent call last):
File "test.py", line 1, in <module>
raise ValueError("Some message")
ValueError: Message here
"""
An addition is used to form exception chains, followed by another exception. This exception will be linked to the generated one in the __cause__
attribute (which supports assignment).
As a result, if the generated exception is not handled, both exceptions will be output.
raise ValueError('Some message') from TypeError('Another message')
"""
Output:
TypeError: Another message
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 1, in <module>
raise ValueError('Some message') from TypeError('Another message')
ValueError: Some message
"""
In general, such a construct is mainly used in conjunction with try-except blocks.
try:
1 / 0
except Exception as exc:
raise RuntimeError('An error occurred') from exc
"""
Output:
Traceback (most recent call last):
File "test.py", line 2, in <module>
1 / 0
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 5, in <module>
raise RuntimeError('An error occurred') from exc
RuntimeError: An error occurred
"""
Assert
When discussing exceptions, it's essential to mention the assert
statement. By default, if you execute a assert
with a logical expression that evaluates to, nothing will happen. However, if you try to execute a assert
statement with a logical expression that evaluates to, an AssertionError
exception will be generated.
assert True
assert 1 == 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
Let's assume we have a function get_user_by_id
that searches for a user by their numeric identifier. We can ensure that a number is actually passed by using assert
and the isinstance
function. If isinstance
returns False
, an AssertionError
will be generated.
It's also possible to pass a string that will serve as an error message, as shown in the example below.
def get_user_by_id(id):
assert isinstance(id, int), "id must be an integer"
print('Performing search')
get_user_by_id("foo")
`AssertionError` exceptions are primarily intended for developers. When writing programs during the development phase, we need to see when we're doing something wrong (e.g., passing an incorrect value to a function). It's unnecessary to handle user input and try to catch `AssertionError` exceptions with a `try-except` block. If there are too many such cases, it could affect the program's performance.
Custom Exceptions
In Python, it's also possible to create custom exception classes. Such a class can, for example, log errors or check certain conditions. What an exception class does is entirely up to us, although most often, it simply displays an error message.
The type of error is important in itself, and we often create our own error types to indicate specific situations not covered by the basic exception classes in Python. This way, users of the class who encounter such an error will know exactly what is happening.
A custom exception class should always inherit from Python's built-in exception class, Exception
.
class MyError(Exception):
pass
Typically, when working, a base exception class is created that inherits from Exception. Then, other classes inherit from this base class. This makes working with exceptions much simpler and more understandable.
class CarError(Exception):
"""Base class for exceptions related to the Car class"""
def __init__(self, car, msg=None):
if msg is None:
# Generates an error message
msg = f'An error occurred with car {car}'
super(CarError, self).__init__(msg)
self.car = car
class CarCrashError(CarError):
"""Raises an exception if a car crashes into another car"""
def __init__(self, car, other_car, speed):
super(CarCrashError, self).__init__(
car, msg=f'Car crashed into {other_car} at speed {speed}'
)
self.speed = speed
self.other_car = other_car
try:
drive_car(car)
except CarCrashError as e:
if e.speed >= 120:
car.brake()
Comments (0)