Chapter 8: Extra Content#

Horizontal lines that break off at different points and fall until a double rule at the foot catches them Horizontal lines that break off at different points and fall until a double rule at the foot catches them

Error Handling (try/except)#

In Python, we use the try/except block to handle errors that may occur during program execution, preventing it from being interrupted unexpectedly.

Basic example with try/except:

try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"The result is {result}.")
except ZeroDivisionError:
    print("Error: division by zero.")
except ValueError:
    print("Error: invalid input.")

Output:

Enter a number: 2
The result is 5.0.
Enter a number: 0
Error: division by zero.
Enter a number: abc
Error: invalid input.

else and finally blocks in error handling#

In addition to try and except, Python allows the optional use of else and finally blocks to make error handling more complete.

else

Executed only if no error occurs in the try block.

finally

Executed always, with or without an error. It is useful for releasing resources, such as closing files or database connections.

Example with else and finally:

try:
    number = int(input("Enter a number: "))
    result = 10 / number
except ZeroDivisionError:
    print("Error: division by zero.")
except ValueError:
    print("Error: invalid input.")
else:
    print(f"The result is {result}.")
finally:
    print("Execution finished.")

Output:

Enter a number: 2
The result is 5.0.
Execution finished.
Enter a number: 0
Error: division by zero.
Execution finished.
Enter a number: xyz
Error: invalid input.
Execution finished.

Using try/except in functions#

It is common to use try/except inside functions to make them safer and more reusable.

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "Error: division by zero."

print(divide(10, 2))  # Output: 5.0
print(divide(10, 0))  # Output: Error: division by zero.

Output:

5.0
Error: division by zero.

Summary Table#

Block

When it is executed

try

Tries to execute code that may error

except

Executes if an error occurs in the try

else

Executes if no error occurs in the try

finally

Always executes, with or without error

File Handling#

So far, our programs’ data lived only in memory: as soon as the program ended, everything was lost. To store information permanently (a report, a to-do list, settings), we write the data to files.

Opening a file: the open() function#

To work with a file, we first open it with open(), giving the path and the mode:

file = open("data.txt", "r", encoding="utf-8")
this example opens a file on your computer in read mode

The mode defines what we can do with the file:

Mode

Meaning

If the file does not exist

"r"

read

raises an error

"w"

write, erases previous content

creates a new one

"a"

append, writes at the end

creates a new one

"x"

exclusive creation

creates; error if it already exists

"b"

binary mode (combine with others, e.g. "rb")

not applicable

The encoding="utf-8" parameter ensures that accented and special characters are read and written correctly.

The right way: the with block#

Every open file must be closed to release system resources. Instead of calling file.close() by hand (and risking forgetting it, or the program failing before that), we use the with block, which closes the file automatically at the end, even if an error occurs:

with open("zen.txt", "w", encoding="utf-8") as file:
    file.write("Beautiful is better than ugly.\n")
    file.write("Explicit is better than implicit.\n")
    file.write("Simple is better than complex.\n")
# outside the with, the file is already closed
this example writes a file to your computer

Best practice - always use with

Always prefer with open(...) as ...:. It handles closing the file for you, avoiding resource leaks and corrupted files. It plays the same role as the finally block we saw in try/except.

Reading a file#

There are a few ways to read the content of a file opened in read mode. The most direct reads everything at once:

with open("zen.txt", "r", encoding="utf-8") as file:
    content = file.read()      # reads the whole file as a single string
    print(content)
this example reads a file from your computer all at once

For large files, reading line by line is more efficient. The file itself is iterable:

with open("zen.txt", "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())   # strip() removes the trailing "\n" of each line
this example reads a file from your computer line by line

Output:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.

Other useful methods are readline() (reads one line at a time) and readlines() (returns a list with all the lines).

Warning - write mode erases the content

Opening an existing file in "w" mode erases all previous content before writing. To add information without losing what is already there, use "a" (append) mode.

Combining with error handling#

File operations fail for many reasons (the file does not exist, lack of permission). That is why files are often combined with try/except:

try:
    with open("missing.txt", "r", encoding="utf-8") as file:
        print(file.read())
except FileNotFoundError:
    print("Error: the file was not found.")
this example tries to read a file from your computer inside a try/except

Output:

Error: the file was not found.

Real-world data: dates, JSON and CSV#

Real projects constantly deal with dates, with structured data (JSON), and with tables and spreadsheets (CSV). Python’s standard library already ships ready-made modules for all three.

Dates and times with datetime#

The datetime module represents dates and times and lets us do arithmetic with them:

from datetime import datetime, timedelta

now = datetime.now()
print(now)                             # e.g. 2024-05-04 14:30:00.123456

# Creating a specific date (year, month, day, hour, minute)
premiere = datetime(1975, 4, 3, 20, 0)
print(premiere.year, premiere.month)   # 1975 4

To display a date in a readable format we use strftime(); to parse a date written as text we use strptime():

from datetime import datetime

now = datetime.now()
print(now.strftime("%d/%m/%Y at %H:%M"))   # e.g. 04/05/2024 at 14:30

text = "03/04/1975"
date = datetime.strptime(text, "%d/%m/%Y")
print(date.year)                            # 1975

The difference between two dates becomes a timedelta, which we use for time arithmetic:

from datetime import datetime, timedelta

tv_premiere = datetime(1969, 10, 5)   # first Monty Python episode
today = datetime.now()
days_elapsed = (today - tv_premiere).days
print(f"{days_elapsed} days have passed.")

in_one_week = today + timedelta(days=7)
print(in_one_week)

Tip - the main format codes

The most common strftime and strptime codes are %d (day), %m (month), %Y (4-digit year), %H (hour), %M (minute) and %S (second).

JSON: saving structured data#

JSON is the most widely used format for exchanging data between programs and on the web. The json module converts between Python dictionaries and lists and JSON text:

import json

character = {
    "name": "King Arthur",
    "movie": "Monty Python and the Holy Grail",
    "has_horse": False,
    "knights": ["Lancelot", "Galahad", "Robin"],
}

# dict -> JSON string
json_text = json.dumps(character, indent=2, ensure_ascii=False)
print(json_text)

# JSON string -> dict
back = json.loads(json_text)
print(back["name"])   # King Arthur

To read and write directly to files, use json.dump() and json.load():

# Write the dictionary to a .json file
with open("character.json", "w", encoding="utf-8") as f:
    json.dump(character, f, indent=2, ensure_ascii=False)

# Read the file back into a dictionary
with open("character.json", "r", encoding="utf-8") as f:
    data = json.load(f)
this example writes and reads a JSON file on your computer

Tip - readable JSON with accents

Use indent=2 to produce indented JSON (easier to read) and ensure_ascii=False to keep accents and special characters instead of codes like á.

Notice the pattern: dumps and loads (with an “s” for string) work with text, while dump and load work directly with files.

CSV: tables and spreadsheets#

CSV (Comma-Separated Values) stores data as a table, with one row per record and values separated by commas. It is the format spreadsheets like Excel export. The csv module reads and writes these files:

import csv

movies = [
    ["title", "year"],
    ["Monty Python and the Holy Grail", 1975],
    ["Life of Brian", 1979],
    ["The Meaning of Life", 1983],
]

# Writing (newline="" avoids extra blank lines)
with open("movies.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(movies)

# Reading back
with open("movies.csv", "r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
this example writes and reads a CSV file on your computer

Output:

['title', 'year']
['Monty Python and the Holy Grail', '1975']
['Life of Brian', '1979']
['The Meaning of Life', '1983']

Note that CSV does not store the data type: the year 1975 comes back from reading as the string '1975'. When each column has a name, csv.DictReader and csv.DictWriter make the code clearer, reading each row as a dictionary.

Going further - the pandas library

For more serious table analysis (filtering, statistics, plots), the pandas library is the standard in the field. The standard-library csv module is enough for simple tasks and requires no installation.

Modules, packages and virtual environments#

As programs grow, we stop writing everything in a single file. We organize the code into modules and packages, and we reuse libraries written by other people.

Modules: reusing code#

A module is simply a .py file. We have already used several standard-library modules, such as random and datetime. To use a module, just import it:

import math

print(math.sqrt(16))    # 4.0
print(math.pi)          # 3.141592653589793

We can import only what we need, or give it an alias with as:

from math import sqrt, pi     # imports specific names
import numpy as np            # alias, common with libraries

Creating your own module#

Any .py file of yours is also a module. Suppose a file greetings.py:

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

PI = 3.14159

In another file, in the same folder, you import it by name (without the .py):

import greetings

print(greetings.hello("Arthur"))   # Hello, Arthur!
this example imports a module that you create yourself

The if __name__ == "__main__" idiom#

When a file can be both run directly and imported by another, we protect the test code with if __name__ == "__main__":. This block only runs when the file is executed directly, not when it is imported:

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

if __name__ == "__main__":
    # Only runs when executing: python greetings.py
    print(hello("world"))

Packages#

A package is a folder that groups several related modules. In practice, it is a directory with .py files (and, traditionally, an __init__.py file). Large libraries such as NumPy and Matplotlib are packages.

Installing libraries with pip#

Not everything ships in the standard library. Thousands of third-party libraries are available on PyPI (Python Package Index) and are installed with pip, Python’s package manager. In the terminal:

pip install requests

Once installed, the library is used like any other:

import requests

response = requests.get("https://api.github.com")
print(response.status_code)   # 200

Tip - pip in Colab and Jupyter

Inside a notebook (such as Google Colab), run pip in a cell prefixed with !, like this: !pip install requests. The ! marks the line as a terminal command, not Python code.

Virtual environments (venv)#

Different projects may need different versions of the same library. To avoid conflicts, we create a virtual environment: an isolated installation of Python and its libraries, separate per project. The venv module does this. In the terminal:

# Creates the environment in a folder named .venv
python -m venv .venv

# Activates the environment
source .venv/bin/activate     # Linux and macOS
.venv\Scripts\activate        # Windows

# With the environment active, pip installs only into it
pip install requests

# When done, deactivate
deactivate

Best practice - one environment per project

Create a virtual environment for each project and avoid installing everything into the system Python. This way, the dependencies of one project do not interfere with another’s, and it is easy to recreate the environment on another machine (for example, by listing the libraries in a requirements.txt file).

A modern alternative: uv#

In recent years, uv has become popular as a fast replacement for pip and venv. Written in Rust by Astral, it is much faster and combines, in a single command, creating environments and installing packages.

Installation (once, on the system):

# Official installer (Linux and macOS)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or, if you already have pip
pip install uv

In daily use, it creates the environment and installs packages much faster than pip:

# Creates the virtual environment (equivalent to python -m venv .venv)
uv venv

# Installs packages (pip-compatible interface)
uv pip install requests

uv also manages the whole project, tracking dependencies in a pyproject.toml file:

uv init my_project       # creates the project structure
cd my_project
uv add requests          # adds a dependency (and updates pyproject.toml)
uv run main.py           # runs the script inside the project environment

Tip - why uv?

uv plays the role of venv and pip (and more) while being dozens of times faster. The uv venv and uv pip install commands are almost identical to the ones you already know, so the transition is easy. It is today the recommended tool for new projects.

Learn more

Official pip and PyPI documentation: https://pypi.org/. The full list of standard-library modules (which need no installation) is at https://docs.python.org/3/library/. uv documentation: https://docs.astral.sh/uv/.