Unit 3: File Handling and Exception Handling

CAP776 — Programming In Python 10 min read

I. Orientation

Python programs interact with two important sources of difficulty: operations may fail during execution, and data may need to be stored outside the program. Exception handling provides a controlled response to runtime errors, while file handling allows programs to create, read, modify, and preserve data in files. Python’s json module extends file handling to structured data represented as JSON text.

  • Runtime principle: Python executes statements sequentially, but errors such as FileNotFoundError or ZeroDivisionError interrupt normal execution unless handled.
  • Exception principle: An exception is an object describing an abnormal event; it can be raised, caught, handled, or allowed to terminate the program.
  • File principle: A file is opened through a file object, used with an operation such as read() or write(), and then closed.
  • Resource convention: with statements are preferred because they close files automatically, including when an exception occurs.
  • Data convention: Text files store characters, whereas JSON files store structured data such as objects, arrays, strings, numbers, Boolean values, and null.
  • Reliability principle: Good programs anticipate specific failures, handle only what they can meaningfully handle, and preserve useful diagnostic information.

II. Built-in exceptions — Python’s predefined error classes

A built-in exception is a predefined Python class representing a common category of runtime failure. Python raises these exceptions automatically when an operation violates its rules.

A. Definition and hierarchy

Built-in exceptions form a class hierarchy, allowing a specific exception to be caught before a broader category such as Exception.

  • Base classes: Most application-level exceptions inherit from Exception; BaseException is higher in the hierarchy and also includes control-flow exceptions such as SystemExit and KeyboardInterrupt.
  • Specificity: ValueError indicates an inappropriate value, while TypeError indicates an inappropriate type.
  • Inheritance example: FileNotFoundError is a subclass of OSError, so either class can catch a missing-file error, although the specific class is clearer.
  • Error message: str(error) commonly provides a human-readable explanation, such as the path that could not be opened.

B. Common built-in exceptions

Recognizing the cause of common exceptions helps select an appropriate handler.

  • NameError: An identifier is not defined; print(total) raises it if total has never been assigned.
  • SyntaxError: Python cannot parse the source, for example if x > 2 without a following colon.
  • TypeError: An operation uses incompatible types; "Age: " + 20 raises TypeError.
  • ValueError: The type is acceptable but the value is not; int("abc") cannot convert the string.
  • IndexError: A sequence index is outside its valid range; [4, 5][2] is invalid.
  • KeyError: A dictionary key is absent; {"a": 1}["b"] raises it.
  • ZeroDivisionError: An arithmetic division uses zero as the divisor, such as 10 / 0.
  • FileNotFoundError: An operation attempts to open a path that does not exist.
  • PermissionError: The operating system denies a requested file operation.
  • JSONDecodeError: The json module cannot interpret text as valid JSON; it is commonly caught when reading malformed JSON.

III. Try and except — controlling runtime failures

try and except provide structured exception handling. Statements that may fail are placed in try, and one or more except clauses define responses for selected exceptions.

A. Purpose and basic syntax

The purpose of try and except is to prevent an anticipated runtime failure from causing uncontrolled program termination.

  • Protected block: The try block contains statements whose exceptions should be handled.
  • Handler block: An except block runs only when a matching exception is raised.
  • Specific handling: Catching ValueError separately from FileNotFoundError allows different corrective actions.
  • Exception object: as error binds the raised exception to a variable for inspection.
PYTHON
try:
    quantity = int(input("Quantity: "))
except ValueError as error:
    print("Enter a whole number:", error)

Here, quantity is intended to hold an integer, and error refers to the ValueError object if conversion fails.

B. else, finally, and multiple handlers

The optional else and finally clauses separate successful processing from cleanup.

  • Multiple handlers: Python checks except clauses from top to bottom; specific exceptions should appear before broad exceptions.
  • else: Runs only when the try block finishes without an exception, making successful processing explicit.
  • finally: Runs whether an exception occurs or not, so it is suitable for essential cleanup such as releasing a resource.
  • Bare except: except: catches nearly everything and can hide programming defects; except Exception: is usually safer but should still be used deliberately.
  • Propagation: If no handler matches, the exception moves to an outer caller; this is called propagation.
PYTHON
try:
    number = int("42")
except ValueError:
    print("Invalid number")
else:
    print(number * 2)
finally:
    print("Conversion attempt finished")

C. Raising and handling exceptions

Programs can use raise when an input violates a rule even though Python’s operation itself succeeds.

  • Validation: raise ValueError("age must not be negative") explicitly rejects an invalid age.
  • Re-raising: A handler can use bare raise to pass the original exception to a higher-level function after logging or cleanup.
  • Chaining: raise RuntimeError("configuration failed") from error preserves the original cause in error.
  • Scope: The try block should be narrow; wrapping an entire program can incorrectly attribute unrelated failures to one operation.

IV. User-defined exceptions — application-specific errors

A user-defined exception is a class created by the programmer for a meaningful error condition that built-in classes do not express precisely.

A. Definition and design

User-defined exceptions communicate domain-specific failures while retaining Python’s standard exception mechanism.

  • Inheritance: A custom exception normally inherits from Exception, not directly from BaseException.
  • Naming: Exception class names conventionally end with Error, such as InsufficientBalanceError.
  • Meaning: A custom class distinguishes a business rule from a technical failure such as TypeError.
  • Optional data: The exception can store useful attributes, such as an account number or rejected amount.
PYTHON
class NegativeMarkError(Exception):
    """Raised when a mark is below zero."""

try:
    mark = -3
    if mark < 0:
        raise NegativeMarkError("Mark cannot be negative")
except NegativeMarkError as error:
    print(error)

Here, NegativeMarkError represents a validation rule, and error contains its message.

B. Use and limitations

Custom exceptions are most useful at clear boundaries between validation, processing, and presentation.

  • Caller control: A calling function can catch NegativeMarkError and display a suitable message without examining text messages.
  • Separation: The exception class defines what went wrong, while the handler decides what to do.
  • Avoid overuse: A custom exception is unnecessary when an existing class, such as ValueError, already communicates the condition accurately.
  • No silent handling: Catching a custom exception should not simply discard the error; the program should correct, report, or propagate it.

V. Text file read/write operations — persistent character data

Text file handling transfers character data between a Python program and a file stored on a device. The standard open() function creates a file object connected to a path.

A. Opening and closing text files

Opening a file requires a path and commonly a mode; encoding should be stated when text portability matters.

  • Path: "notes.txt" identifies the file; a relative path depends on the program’s current working directory.
  • Modes:
    • r reads an existing file.
    • w writes and truncates an existing file or creates a new file.
    • a appends to the end or creates the file.
    • x creates a new file and fails if it already exists.
  • Encoding: encoding="utf-8" specifies how characters become bytes and back again.
  • Automatic closure: with open(...) as file: closes the file after the block.
PYTHON
with open("greeting.txt", "w", encoding="utf-8") as file:
    file.write("Hello, Python!\n")

The string written contains a newline character, and the file is closed automatically.

B. Reading text

Reading methods differ in how much content they return and how they represent line boundaries.

  • read(): Returns the entire remaining file as one string; file.read(5) returns at most five characters.
  • readline(): Returns one line, usually including its trailing newline.
  • readlines(): Returns a list containing the remaining lines.
  • Iteration: for line in file processes one line at a time and is more memory-efficient for large files.
  • Whitespace: line.strip() removes leading and trailing whitespace, including the newline; use it only when that removal is intended.
PYTHON
with open("scores.txt", "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

C. Writing and appending text

Writing methods place strings into a file; they do not automatically convert arbitrary objects into text.

  • write(): Writes one string and returns the number of characters written; file.write("A\n") writes two characters.
  • writelines(): Writes an iterable of strings without adding separators, so each item should contain its own \n when line breaks are required.
  • Conversion: file.write(str(25)) is valid, whereas file.write(25) raises TypeError.
  • Truncation risk: Mode w erases previous content before writing; mode a preserves it and adds new content.
  • Error handling: Opening a missing file in r mode raises FileNotFoundError; an unavailable location may raise PermissionError.

D. File handling applications and limitations

Text files are simple and human-readable, but their structure must be designed and interpreted consistently.

  • Applications: Logs, configuration fragments, reports, and line-based records can be stored as text.
  • Newline handling: newline behavior can differ across operating systems; normal text mode generally translates line endings appropriately.
  • Large files: Iteration avoids loading the entire file into memory, unlike read().
  • Atomicity limitation: A program interrupted during a rewrite may leave incomplete content; temporary-file replacement is safer for critical data.
  • Security: Never treat file contents as trusted input; validate data before using it.

VI. Reading JSON files — structured data exchange

JSON, or JavaScript Object Notation, is a text format for representing structured values. Python’s json module converts between JSON text and Python objects.

A. JSON structure and Python mapping

JSON supports a limited set of data types that map directly to Python values.

  • Object mapping: A JSON object such as {"name": "Mira", "age": 20} becomes a Python dict.
  • Array mapping: A JSON array such as [10, 20, 30] becomes a Python list.
  • Primitive mapping: JSON strings become str, numbers become int or float, true and false become True and False, and null becomes None.
  • Restriction: JSON object keys must be strings; Python dictionaries with non-string keys may not round-trip as expected.
  • Validity: JSON requires valid syntax, including double-quoted strings and correctly matched brackets.

B. json.load() and file reading

json.load() reads JSON directly from an open text file and returns the corresponding Python object.

  • Opening: Use read mode and UTF-8 encoding for a JSON text file.
  • Conversion: json.load(file) parses the complete file, so the resulting value may be a dictionary, list, string, number, Boolean, or None.
  • Failures: Missing files raise FileNotFoundError; invalid JSON raises json.JSONDecodeError.
  • Validation: Parsing confirms JSON syntax, not that required application fields or value ranges are correct.
PYTHON
import json

try:
    with open("student.json", "r", encoding="utf-8") as file:
        student = json.load(file)
    print(student["name"])
except FileNotFoundError:
    print("The JSON file is missing")
except json.JSONDecodeError:
    print("The JSON syntax is invalid")

C. Processing and limitations

Reading JSON is useful when programs need named, nested, and portable data, but parsed data must still be checked.

  • Field access: student["name"] requires the "name" key; a missing key raises KeyError.
  • Safer lookup: student.get("grade") returns None by default when "grade" is absent.
  • Type validation: Confirm that student is a dictionary and that student["age"] is numeric before calculations.
  • Memory limitation: json.load() normally builds the entire structure in memory; very large datasets may require streaming formats or incremental processing.
  • Trust boundary: JSON parsing does not make data safe; validate values and avoid assuming that external files follow the expected schema.