Unit 3: File Handling, Packaging, and Debugging

3.1 File Handling - Reading and Writing Files, Exception Handling

Opening Files

Python uses the open() function to work with files. The basic syntax is:

file = open("filename", "mode")

File Modes:

Mode Description
'r' Read (default) - Opens file for reading
'w' Write - Creates new file or overwrites existing
'a' Append - Adds content at the end of file
'x' Create - Creates new file, fails if exists
'r+' Read and Write
'b' Binary mode (e.g., 'rb', 'wb')
't' Text mode (default)

Reading Files

# Reading entire file
file = open("example.txt", "r")
content = file.read()
print(content)
file.close()

# Reading line by line
file = open("example.txt", "r")
for line in file:
    print(line.strip())
file.close()

# Reading specific number of characters
file = open("example.txt", "r")
content = file.read(10)  # Read first 10 characters
print(content)
file.close()

# Reading all lines into a list
file = open("example.txt", "r")
lines = file.readlines()
print(lines)
file.close()

Writing Files

# Writing to a file (overwrites existing content)
file = open("output.txt", "w")
file.write("Hello, World!\n")
file.write("This is a new line.")
file.close()

# Appending to a file
file = open("output.txt", "a")
file.write("\nThis line is appended.")
file.close()

# Writing multiple lines
lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
file = open("output.txt", "w")
file.writelines(lines)
file.close()

Using 'with' Statement (Context Manager)

The with statement automatically handles file closing:

# Reading with 'with' statement
with open("example.txt", "r") as file:
    content = file.read()
    print(content)
# File is automatically closed here

# Writing with 'with' statement
with open("output.txt", "w") as file:
    file.write("Hello, World!")

Exception Handling

Python uses try-except blocks to handle errors gracefully:

try:
    # Code that might raise an exception
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero!")

# Multiple exceptions
try:
    file = open("nonexistent.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found!")
except PermissionError:
    print("Permission denied!")
except Exception as e:
    print(f"An error occurred: {e}")

try-except-else-finally

try:
    file = open("example.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found!")
else:
    # Executes if no exception occurred
    print("File read successfully!")
    print(content)
finally:
    # Always executes (cleanup code)
    print("Execution completed.")
    if 'file' in locals():
        file.close()

Raising Exceptions

def check_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative!")
    if age < 18:
        raise Exception("Must be 18 or older!")
    return "Access granted"

try:
    result = check_age(-5)
except ValueError as e:
    print(f"Value Error: {e}")
except Exception as e:
    print(f"Error: {e}")

Common Built-in Exceptions

Exception Description
FileNotFoundError File or directory not found
PermissionError Permission denied for file operation
IOError Input/Output operation failed
ValueError Invalid value or type
TypeError Operation on incompatible types
ZeroDivisionError Division by zero
IndexError Index out of range
KeyError Key not found in dictionary

3.2 Creating Python Packages, Modules and Executable Files

Modules

A module is a Python file containing functions, classes, and variables that can be imported into other programs.

Creating a Module (mymodule.py):

# mymodule.py
def greet(name):
    return f"Hello, {name}!"

def add(a, b):
    return a + b

PI = 3.14159

class Calculator:
    def multiply(self, a, b):
        return a * b

Using the Module:

# main.py
import mymodule

print(mymodule.greet("Alice"))
print(mymodule.add(5, 3))
print(mymodule.PI)

calc = mymodule.Calculator()
print(calc.multiply(4, 5))

# Import specific items
from mymodule import greet, PI
print(greet("Bob"))
print(PI)

# Import with alias
import mymodule as mm
print(mm.add(10, 20))

# Import all (not recommended)
from mymodule import *

Packages

A package is a directory containing multiple modules and a special __init__.py file.

Package Structure:

mypackage/
    __init__.py
    module1.py
    module2.py
    subpackage/
        __init__.py
        module3.py

__init__.py file:

# mypackage/__init__.py
from .module1 import function1
from .module2 import function2

__all__ = ['function1', 'function2']

Using Packages:

# Importing from package
from mypackage import module1
from mypackage.module1 import function1
from mypackage.subpackage import module3

# Using the imported functions
result = function1()
module1.another_function()

The __name__ Variable

Used to determine if a file is run directly or imported:

# mymodule.py
def main():
    print("Running as main program")

if __name__ == "__main__":
    main()
# This code runs only when the file is executed directly
# Not when it's imported as a module

Creating Executable Files

Python scripts can be converted to standalone executables using tools like PyInstaller:

Installing PyInstaller:

pip install pyinstaller

Creating an Executable:

# Basic executable
pyinstaller myscript.py

# Single file executable
pyinstaller --onefile myscript.py

# Without console window (for GUI apps)
pyinstaller --onefile --noconsole myscript.py

# With custom icon
pyinstaller --onefile --icon=myicon.ico myscript.py

Built-in Modules

Module Description
os Operating system interface
sys System-specific parameters
math Mathematical functions
datetime Date and time handling
json JSON encoding/decoding
random Random number generation
re Regular expressions

3.3 Dealing with Syntax Errors, Runtime Errors and Scientific Debugging

Types of Errors

1. Syntax Errors

Errors in the structure of the code that prevent it from running:

# Missing colon
if x > 5
    print("x is greater")  # SyntaxError

# Incorrect indentation
def my_function():
print("Hello")  # IndentationError

# Unmatched parentheses
print("Hello"  # SyntaxError

# Invalid syntax
x = 5 +  # SyntaxError

Common Syntax Errors:

  • Missing colons after if, for, while, def, class statements
  • Incorrect indentation
  • Unmatched parentheses, brackets, or quotes
  • Using reserved keywords as variable names
  • Missing commas in lists or function arguments

2. Runtime Errors (Exceptions)

Errors that occur during program execution:

# ZeroDivisionError
result = 10 / 0

# TypeError
result = "5" + 5

# IndexError
my_list = [1, 2, 3]
print(my_list[10])

# KeyError
my_dict = {"name": "John"}
print(my_dict["age"])

# NameError
print(undefined_variable)

# AttributeError
x = 5
x.append(10)  # int has no append method

3. Logical Errors

Code runs without errors but produces incorrect results:

# Incorrect logic - should be average, not sum
def calculate_average(numbers):
    return sum(numbers)  # Missing division by length

# Off-by-one error
for i in range(10):  # Should be range(11) to include 10
    print(i)

Scientific Debugging Techniques

1. Print Debugging

def calculate_total(items):
    total = 0
    print(f"Starting calculation with {len(items)} items")
    for item in items:
        print(f"Processing item: {item}")
        total += item
        print(f"Running total: {total}")
    print(f"Final total: {total}")
    return total

2. Using the Python Debugger (pdb)

import pdb

def buggy_function(x, y):
    pdb.set_trace()  # Debugger stops here
    result = x + y
    return result

# pdb commands:
# n - next line
# s - step into function
# c - continue execution
# p variable - print variable value
# l - list source code
# q - quit debugger

3. Using breakpoint() (Python 3.7+)

def my_function(data):
    breakpoint()  # Built-in debugger
    processed = process_data(data)
    return processed

4. Assertions

def calculate_average(numbers):
    assert len(numbers) > 0, "List cannot be empty"
    assert all(isinstance(n, (int, float)) for n in numbers), "All items must be numbers"
    return sum(numbers) / len(numbers)

# Assertions help catch bugs early
try:
    avg = calculate_average([])
except AssertionError as e:
    print(f"Assertion failed: {e}")

5. Logging

import logging

# Configure logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def process_data(data):
    logging.debug(f"Input data: {data}")
    logging.info("Starting data processing")
    
    try:
        result = data * 2
        logging.info(f"Processing successful: {result}")
        return result
    except Exception as e:
        logging.error(f"Processing failed: {e}")
        raise

# Logging levels: DEBUG, INFO, WARNING, ERROR, CRITICAL

6. Unit Testing

import unittest

def add(a, b):
    return a + b

class TestAddFunction(unittest.TestCase):
    def test_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)
    
    def test_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)
    
    def test_mixed_numbers(self):
        self.assertEqual(add(-1, 1), 0)

if __name__ == '__main__':
    unittest.main()

Debugging Best Practices

  • Reproduce the bug: Understand the exact conditions that cause the error
  • Isolate the problem: Narrow down the code section causing the issue
  • Read error messages carefully: Python provides detailed traceback information
  • Use version control: Track changes to identify when bugs were introduced
  • Write tests: Prevent regression by testing after fixes
  • Rubber duck debugging: Explain your code line by line to find issues
  • Take breaks: Fresh eyes often spot problems faster

Understanding Tracebacks

Traceback (most recent call last):
  File "main.py", line 10, in <module>
    result = calculate(5, 0)
  File "main.py", line 5, in calculate
    return a / b
ZeroDivisionError: division by zero

# Reading traceback:
# 1. Start from the bottom - shows the actual error
# 2. Work upward to trace the call stack
# 3. Line numbers help locate the issue