Python Programming
Exception Handling & File Handling
15-Mark Answers • Detailed Explanations • Code Examples
TOPIC 1: Exception Handling in Python
1. What is an Exception?
An exception is an event that disrupts the normal flow of a program's execution. In Python, when
an error occurs during runtime, the interpreter generates an exception object that contains
information about the type of error and where it occurred.
Unlike syntax errors — which are detected during the parsing phase before execution begins —
exceptions occur during execution when the interpreter encounters an operation it cannot perform.
If an exception is not handled, it propagates up through the call stack until it either reaches an
exception handler or terminates the program with a traceback message.
Python's exception mechanism is built on an object-oriented hierarchy. Every exception is an
instance of a class that inherits (directly or indirectly) from the base class BaseException. Most
user-facing exceptions inherit from Exception, which itself inherits from BaseException.
Exception Class Hierarchy:
BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
├── ArithmeticError
│ └── ZeroDivisionError
├── OSError
│ └── FileNotFoundError
├── ValueError
├── TypeError
└── ... (many more)
Python Study Guide | Page 1 of 13
2. Types of Errors
Syntax Errors are raised by the parser when the source code violates the grammatical rules of
Python. These are detected before the program runs. The interpreter will not execute a single line
of code if a syntax error exists anywhere in the module.
Runtime Errors (Exceptions) occur during execution when the interpreter encounters a situation it
cannot handle — such as accessing an out-of-bounds index, performing arithmetic on incompatible
types, or referencing an undefined name. These are what exception handling is designed to
manage.
Logical Errors produce incorrect results without raising any exception. The program executes
completely but the logic implemented by the programmer does not achieve the intended outcome.
The interpreter has no mechanism to detect these — they require debugging by the programmer.
3. The try–except Block
The try–except construct is the fundamental mechanism for exception handling in Python. It
separates the normal execution path from the error handling path, improving code clarity and
robustness.
How it works internally:
When Python enters a try block, it registers an exception handler for that scope. If any statement
within the try block raises an exception, Python immediately stops executing the remaining
statements in the try block, searches for a matching except clause, and transfers control to it. If no
matching handler is found in the current scope, Python unwinds the call stack and searches in the
calling function, continuing until it either finds a handler or reaches the top level and terminates.
Syntax:
try:
# statements that may raise an exception
except ExceptionType:
# handler executed if ExceptionType is raised
Example:
try:
x = int(input('Enter a number: '))
result = 100 / x
except ZeroDivisionError:
print('Division by zero is undefined.')
except ValueError:
print('Input was not a valid integer.')
Python Study Guide | Page 2 of 13
Catching multiple exceptions in a single except clause is also possible using a tuple:
except (ZeroDivisionError, ValueError) as e:
print(f'Error occurred: {e}')
The as e syntax binds the exception instance to the variable e, giving access to the exception's
message and attributes.
N A bare except clause — catching all exceptions without specifying a type — is generally
O discouraged because it also catches system-level exceptions like SystemExit and
T KeyboardInterrupt, potentially masking serious problems. The preferred approach is to catch
E Exception if you need a broad catch.
4. The else Block
The else clause in a try–except structure is a block that executes only if no exception was
raised in the try block. It is semantically distinct from simply placing code after the except block,
because code after the except block runs regardless of whether an exception occurred — whereas
the else block runs only in the absence of an exception.
Syntax:
try:
risky_operation()
except SomeException:
handle_error()
else:
# runs only if try completed without any exception
post_success_operation()
Why use else instead of putting code in try?
Placing additional code inside the try block unnecessarily expands the scope of exception
catching. If that additional code raises an exception of the same type you are already handling, it
would be incorrectly caught by the except clause. The else block prevents this by keeping the try
block minimal — containing only the code that is expected to fail — while the else block handles
logic that should execute only on success.
Example:
try:
f = open('[Link]', 'r')
except FileNotFoundError:
Python Study Guide | Page 3 of 13
print('File does not exist.')
else:
content = [Link]() # only runs if file opened successfully
[Link]()
print(content)
5. Handling ZeroDivisionError
ZeroDivisionError is a subclass of ArithmeticError, which is itself a subclass of Exception. It
is raised when the second operand in a division or modulo operation is zero. This applies to all
division operators: / (true division), // (floor division), and % (modulo).
Why does this exception exist?
In mathematics, division by zero is undefined — there is no number that when multiplied by zero
gives a non-zero dividend. Allowing the operation would produce an invalid result, so Python raises
an exception instead.
print(10 / 0) # ZeroDivisionError: division by zero
print(10 // 0) # ZeroDivisionError: integer division or modulo by zero
print(10 % 0) # ZeroDivisionError: integer division or modulo by zero
Handling it properly:
def divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print(f'ZeroDivisionError caught: {e}')
return None
else:
return result
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # ZeroDivisionError caught: division by zero
6. Handling FileNotFoundError
FileNotFoundError is a subclass of OSError (also known as IOError in older Python versions). It
is raised when a file operation — most commonly open() in read mode — is attempted on a path
that does not exist in the filesystem.
Inheritance chain:
Python Study Guide | Page 4 of 13
BaseException → Exception → OSError → FileNotFoundError
OSError is the base class for all operating system related errors. FileNotFoundError corresponds
to the POSIX errno code ENOENT (Error NO ENTry).
try:
with open('[Link]', 'r') as f:
data = [Link]()
except FileNotFoundError as e:
print(f'Error: {e}')
print(f'Error code: {[Link]}') # 2 (ENOENT)
print(f'Filename: {[Link]}') # '[Link]'
The exception object for FileNotFoundError carries useful attributes: errno (the numeric OS error
code), strerror (human-readable error description), and filename (the path that triggered the
error).
Comprehensive example combining both exceptions:
try:
filename = input('Enter filename: ')
with open(filename, 'r') as f:
value = int([Link]().strip())
result = 100 / value
except FileNotFoundError as e:
print(f'File not found: {[Link]}')
except ZeroDivisionError:
print('File contains 0 — cannot divide by zero.')
except ValueError:
print('File content is not a valid integer.')
else:
print(f'Result: {result}')
finally:
print('Execution complete.')
N The finally block always executes regardless of whether an exception was raised or handled. It is
O used for cleanup operations that must occur no matter what — such as closing database
T connections, releasing locks, or closing network sockets. Even if the except block itself raises an
E exception, finally still runs.
7. Common Built-in Exceptions
Python Study Guide | Page 5 of 13
Exception Inherits From Raised When
ZeroDivisionError ArithmeticError Division or modulo by zero
FileNotFoundError OSError File or directory not found
ValueError Exception Correct type but inappropriate value
TypeError Exception Operation on incompatible types
IndexError LookupError Sequence index out of range
KeyError LookupError Dictionary key not found
NameError Exception Undefined variable referenced
AttributeError Exception Attribute reference or assignment fails
OverflowError ArithmeticError Result too large to represent
RecursionError RuntimeError Maximum recursion depth exceeded
Python Study Guide | Page 6 of 13
TOPIC 2: File Handling in Python
1. Introduction to File Handling
File handling refers to the set of operations that allow a program to interact with files stored on a
persistent storage medium such as a hard disk or SSD. Unlike data stored in RAM — which is
volatile and is lost when the program terminates or the system powers off — data stored in files
persists across sessions.
Python provides a built-in I/O (Input/Output) system that abstracts the underlying operating system
file operations. When Python performs file I/O, it interacts with the OS through system calls (open,
read, write, close at the C level), and the OS in turn manages the actual read/write operations on
the physical storage device via the filesystem.
Python's file I/O system also implements buffering by default. Rather than writing each byte to disk
immediately (which would be extremely slow due to disk latency), Python accumulates data in an
in-memory buffer and flushes it to disk in larger, efficient chunks. This is why explicitly closing a file
(or using flush()) is important — it ensures the buffer is flushed and all data is actually written to
disk.
2. Types of Files
Text Files store data encoded as a sequence of characters using a character encoding scheme
such as UTF-8 or ASCII. In Python, when you open a file in text mode, the I/O layer handles
encoding and decoding automatically. Additionally, Python performs newline translation — on
Windows, the OS uses \r\n as the line terminator, but Python's text mode translates this to \n on
reading and back to \r\n on writing, providing platform-independent behavior.
Binary Files store raw bytes with no encoding or newline translation. When you open a file in
binary mode ("rb", "wb"), Python passes the bytes directly to and from the file without any
transformation. This is essential for files that contain non-text data such as images (PNG, JPEG),
audio (MP3, WAV), compiled executables, or serialized objects (pickle files).
W Applying text-mode operations to binary files (or vice versa) corrupts the data. For example, reading
A a JPEG in text mode would cause Python to attempt UTF-8 decoding on raw binary data, raising a
R UnicodeDecodeError.
N
Python Study Guide | Page 7 of 13
3. Opening and Closing a File
The built-in open() function is the entry point for all file operations. It requests the OS to open the
specified file and returns a file object (also called a file handle or stream object) that provides
methods to interact with the file.
Full signature:
open(file, mode='r', buffering=-1, encoding=None,
errors=None, newline=None, closefd=True, opener=None)
Mode flags:
Mode Description
"r" Read (default). File must exist.
"w" Write. Creates file or truncates existing file to zero length.
"a" Append. Creates file if absent; writes at end if present.
"x" Exclusive creation. Fails with FileExistsError if file exists.
"b" Binary mode. Combined with r/w/a (e.g., "rb", "wb").
"t" Text mode (default). Combined with r/w/a.
"+" Open for updating (reading and writing).
What open() does internally: It invokes the OS open system call, which allocates a file descriptor
— a small non-negative integer that the OS uses to track open files for that process. Python wraps
this in a file object. The file descriptor can be accessed via file_object.fileno().
Closing a file: [Link]() flushes the internal buffer, writes any pending data to disk, and
releases the file descriptor back to the OS. Failing to close a file may cause data loss (unflushed
buffer), resource leaks (file descriptor leak), and on some OSes, prevent other processes from
accessing the file.
The with statement (Context Manager):
The recommended approach uses Python's context manager protocol. The open() function returns
an object that implements __enter__() and __exit__(). The with block calls __enter__() on
entry and guarantees __exit__() is called on exit — even if an exception is raised — which in turn
calls close().
with open('[Link]', 'r') as f:
data = [Link]()
# [Link]() is called automatically here, even if an exception occurred
Python Study Guide | Page 8 of 13
4. Writing to a File
Writing requires opening the file in "w", "a", or "x" mode (or "r+" for read-write on existing files).
write(string) writes the given string to the file and returns the number of characters written. It does
not append a newline — the programmer must explicitly include \n where needed. The data may
initially go into the I/O buffer and is flushed to disk either when the buffer is full, when flush() is
called, or when the file is closed.
writelines(iterable) accepts any iterable of strings and writes each element sequentially to the file.
It is not restricted to lists — any iterable works. Like write(), it does not insert newlines between
elements.
with open('[Link]', 'w') as f:
[Link]('Session started\n')
[Link]('User: admin\n')
entries = ['ERROR: null pointer\n', 'WARNING: low memory\n', 'INFO: process
complete\n']
with open('[Link]', 'a') as f:
[Link](entries)
Difference between "w" and "a": Opening in "w" mode calls the OS truncate system call, which
sets the file's length to zero before writing begins — all previous content is destroyed. Opening in
"a" mode moves the file pointer to the end of the file before every write operation, preserving
existing content.
5. Reading from a File
Reading requires opening the file in "r" mode (or "r+" for read-write).
read(size=-1) reads and returns up to size bytes from the file as a string (in text mode) or bytes
object (in binary mode). If size is omitted or -1, it reads the entire file from the current pointer
position to the end. For large files, this loads all content into memory simultaneously, which can be
a concern.
readline(size=-1) reads one complete line — up to and including the \n character — and returns it
as a string. If the end of the file is reached and no newline was found, it returns whatever remains.
Returns an empty string "" when the pointer is at the end of the file (EOF), which is how a loop can
detect the end of a file using readline().
Python Study Guide | Page 9 of 13
readlines(hint=-1) reads all remaining lines and returns them as a list of strings, each string
including its terminating \n. The optional hint parameter specifies an approximate total byte count
— once that many bytes have been read, readlines() stops, even if it's mid-file.
with open('[Link]', 'r') as f:
all_text = [Link]() # entire file as one string
with open('[Link]', 'r') as f:
first_line = [Link]() # reads 'line 1\n'
second_line = [Link]() # reads 'line 2\n'
with open('[Link]', 'r') as f:
line_list = [Link]() # ['line 1\n', 'line 2\n', ...]
Iterating directly over the file object is often the most Pythonic and memory-efficient approach for
line-by-line reading, as it reads one line at a time without loading the entire file:
with open('[Link]', 'r') as f:
for line in f:
process([Link]('\n'))
6. Setting Offset in a File — tell() and seek()
Python's file I/O maintains an internal file position indicator (the file pointer or offset) that tracks
the byte position at which the next read or write operation will occur. It starts at 0 when the file is
opened (except in append mode, where writes always go to the end regardless of pointer position).
tell() returns the current byte offset of the file pointer as an integer. In text mode on systems with
multi-byte encodings, this value is opaque — it can be passed back to seek() but should not be
interpreted as a raw byte count.
seek(offset, whence=0) repositions the file pointer. The whence parameter specifies the reference
point:
• 0 (SEEK_SET) — from the beginning of the file
• 1 (SEEK_CUR) — from the current position
• 2 (SEEK_END) — from the end of the file
N In text mode, seek() is restricted: only seek(0, 0) (go to beginning) and values returned by tell() are
O guaranteed to work correctly. Arbitrary byte offsets in text mode can misalign the pointer within a
T multi-byte character, causing decoding errors. In binary mode, seek() works freely with any integer
E offset.
Python Study Guide | Page 10 of 13
with open('[Link]', 'wb') as f:
[Link](b'ABCDEFGHIJ') # 10 bytes
with open('[Link]', 'rb') as f:
print([Link](3)) # b'ABC', pointer now at 3
print([Link]()) # 3
[Link](0) # back to start
print([Link](3)) # b'ABC' again
[Link](-3, 2) # 3 bytes before end
print([Link]()) # b'HIJ'
[Link](2, 1) # 2 bytes forward from current position
tell() and seek() are particularly important for random access — the ability to jump to any
position in a file without reading sequentially from the beginning. This is used extensively in
database engines, binary file parsers, and media file processing.
7. Creating and Traversing a File
Creating a file uses either "w" or "x" mode. The key distinction is safety: "w" silently destroys
existing content, while "x" raises FileExistsError if the file is already present, making it the
appropriate choice when you must not overwrite existing data.
import os
filename = '[Link]'
# Safe creation — will not overwrite
if not [Link](filename):
with open(filename, 'w') as f:
[Link]('Initial content\n')
else:
print('File already exists, skipping creation.')
# Alternatively using 'x' mode with exception handling
try:
with open(filename, 'x') as f:
[Link]('Initial content\n')
except FileExistsError:
print(f"'{filename}' already exists.")
Traversing a file refers to sequentially processing each line of the file. The most efficient method is
direct iteration over the file object. This uses Python's iterator protocol — the file object
implements __iter__() and __next__(), where each call to __next__() reads and returns the
next line. This is memory-efficient because only one line is held in memory at a time, making it
suitable for files of arbitrary size.
Python Study Guide | Page 11 of 13
with open('[Link]', 'w') as f:
for i in range(1, 6):
[Link](f'Record {i}: value={i*10}\n')
# Traverse: process each line individually
with open('[Link]', 'r') as f:
for line_number, line in enumerate(f, start=1):
stripped = [Link]('\n')
print(f'Line {line_number}: {stripped}')
Using enumerate() pairs each line with its line number, which is useful for debugging, error
reporting, and structured data parsing.
Complete program — demonstrating all file handling operations:
import os
filename = 'student_records.txt'
# 1. CREATE and WRITE
with open(filename, 'w') as f:
students = [
'Alice,85,A\n',
'Bob,72,B\n',
'Charlie,91,A+\n',
'Diana,60,C\n'
]
[Link](students)
# 2. APPEND a new record
with open(filename, 'a') as f:
[Link]('Eve,88,A\n')
# 3. SEEK and TELL
with open(filename, 'r') as f:
[Link](0, 2) # seek to end
file_size = [Link]() # get total size
print(f'File size: {file_size} bytes')
[Link](0)
header_sample = [Link](10)
print(f'First 10 chars: {repr(header_sample)}')
# 4. TRAVERSE line by line
print('\n--- Student Records ---')
with open(filename, 'r') as f:
for idx, line in enumerate(f, start=1):
name, marks, grade = [Link]().split(',')
print(f'{idx}. {name} | Marks: {marks} | Grade: {grade}')
# 5. CLEANUP
[Link](filename)
Python Study Guide | Page 12 of 13
Summary of File Handling Operations
Operation Function / Method Mode Required
Open file open(path, mode) Any
Write string [Link](str) w, a, x, r+
Write list [Link](list) w, a, x, r+
Read all [Link]() r
Read one line [Link]() r
Read all lines [Link]() r
Get pointer position [Link]() Any
Move pointer [Link](offset, whence) Any
Close file [Link]() Any
Flush buffer [Link]() Any
Python Study Guide | Page 13 of 13