Build a Flight Alert System in Python

Fetch live flight prices and get emailed when a deal drops below your target.

Introduction

30 Second Summary

You know that feeling of checking flight prices every day, hoping for a drop that never seems to come? Imagine a script that does that watching for you and sends you an email the second a deal appears.

In this project, you will build a Python flight alert system that monitors Google Flights prices for routes you choose and emails you when a price drops below your target.

What You'll Build

You type one command and your script checks every route on your watchlist against Google Flights, then a formatted email lands in your inbox with airline names, prices, and durations the moment a deal surfaces.

By the end of this project, you'll have:

  • A live flight search that pulls real prices from Google Flights for any route and displays the airline, price, duration, and number of stops in your terminal.
  • A multi-route watchlist with per-route target prices, so only genuine deals below your threshold trigger an alert.
  • An automated email notification that fires when prices drop, formatted with everything you need to book fast.
  • Secret Mission: A price history tracker that saves prices to a JSON file and tells you whether today's price is above or below the 7-day average.

Are there any prerequisites?

You should have basic Python familiarity (variables, functions, loops). No flight API accounts or signups are needed for this project.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Environment

Before you can pull flight prices, you need the right tools installed. The faster-flights library talks to Google Flights for you, but it needs Python 3.10+ and a clean environment to run in.

In this step, you'll create a dedicated project folder, set up a virtual environment to keep dependencies isolated, and install the library that fetches live flight data.

In this step, get ready to:
  • Confirm Python 3.10+ is installed on your machine.
  • Create a project folder with a virtual environment.
  • Install the faster-flights library.
Confirm your Python version
  • Confirm Python is ready by running this command in your terminal:
python3 --version

You should see a version number of 3.10 or higher printed in the terminal.

Don't see Python 3.10+?

If you see a version lower than 3.10, or get a "command not found" error, download and install the latest Python from python.org.

On macOS, you can also install via Homebrew:

brew install python

After installing, close and reopen your terminal, then run python3 --version again.

help me install Python 3.10 or higher on my machine

Create your project folder and virtual environment

A virtual environment gives your project its own isolated set of packages, so nothing conflicts with other Python projects on your machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and move into it by running these commands:
mkdir flight-alerts
cd flight-alerts
  • Confirm you're in the right place by running:
pwd

You should see a path ending in flight-alerts, like /Users/yourname/Desktop/flight-alerts.

  • Create a virtual environment inside this folder by running:
python3 -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

Your terminal prompt now shows (venv) at the beginning of the line. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment is a self-contained folder that holds its own copy of pip and any packages you install. Packages installed here stay separate from your system Python, so different projects can use different library versions without conflict.

Don't see (venv) in your prompt?

Make sure you ran the activation command from inside the flight-alerts folder where you created the venv directory.

On Windows PowerShell, you may need to allow script execution first. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser and then try the activate command again.

help me activate my Python virtual environment

Install the faster-flights library
  • Install the faster-flights library by running:
pip install faster-flights==3.8.0

You'll see pip download and install several packages. Once it finishes, you should see a "Successfully installed" message near the bottom of the output.

What is faster-flights?

This library fetches real flight prices from Google Flights without needing an API key or account signup. It returns structured results including airline names, prices, durations, and number of stops.

Seeing an install error?

Make sure (venv) appears in your prompt before running pip install. If it does not, re-run the activation command from the previous substep.

If you see a permissions error, you are likely installing into your system Python instead of the virtual environment.

help me fix a pip install error for faster-flights

Before you run the verification command, what do you think will happen if the library installed correctly?

  • Verify the library is working by running:
python3 -c "from fast_flights import get_flights; print('Ready!')"

You should see Ready! printed in your terminal. This confirms the faster-flights library is installed and importable.

Seeing a ModuleNotFoundError?

This means Python cannot find the fast_flights module. Double-check that (venv) is showing in your prompt and that pip install faster-flights==3.8.0 completed without errors.

Note that the package is installed as faster-flights but imported as fast_flights. These names are different by design.

help me fix a ModuleNotFoundError for fast_flights

Your environment is ready to go. Next up, you'll use the faster-flights library to pull live flight prices from Google Flights and display them in your terminal.

Fetch Live Flight Prices

Your environment is set up with Python and the faster-flights library installed. Now it's time to put that library to work and pull real data from Google Flights.

Before you can build any alerting logic, you need to prove you can fetch live pricing data and understand what the library returns. In this step, you'll write a reusable search function and format the results into something readable.

In this step, get ready to:
  • Build a reusable flight search function that queries Google Flights for any route.
  • Format and display flight results showing airline, price, duration, and stops.
Write the flight search function

The faster-flights library talks to Google Flights using an internal RPC instead of scraping HTML. You build a query describing your route, then pass it to get_flights() to get back a list of flight options sorted by price.

  • Create a new file called search_flights.py in your flight-alerts folder by running:
touch search_flights.py
  • Confirm the file was created by running:
ls

You should see search_flights.py listed alongside your venv folder.

  • Open search_flights.py in your code editor and add the following search function:
from fast_flights import FlightQuery, Passengers, ShoppingOptions, create_query, get_flights


def search_route(origin, destination, date):
    # Build a query for a one-way economy flight
    query = create_query(
        flights=[
            FlightQuery(
                date=date,
                from_airport=origin,
                to_airport=destination,
            ),
        ],
        trip="one-way",
        seat="economy",
        passengers=Passengers(adults=1),
        language="en-US",
        currency="USD",
    )

    # Fetch results sorted by cheapest price
    results = get_flights(
        query,
        shopping=ShoppingOptions(
            ranking_mode="cheapest",
            result_sort="price",
        ),
    )

    return results

What does this code do?

This function wraps the entire flight search into a reusable call that takes three arguments: an origin airport code, a destination airport code, and a travel date.

  • create_query() builds the request payload with your route details, seat class, and passenger count.
  • FlightQuery specifies a single flight leg using IATA airport codes (3-letter codes like JFK, LHR, LAX).
  • ShoppingOptions with ranking_mode="cheapest" and result_sort="price" tells Google Flights to return results ordered from lowest to highest price.
  • Save search_flights.py.
Display flight results in a readable format

The search_route() function returns a list of flight objects. Each flight has attributes like airlines, price, duration, and stops. Now you need a way to actually see that data when you run the script.

  • Add the following code at the bottom of search_flights.py, below the return results line:
if __name__ == "__main__":
    results = search_route("JFK", "LHR", "2026-09-15")

    print(f"\n{'='*60}")
    print(f"Flights from JFK to LHR on 2026-09-15")
    print(f"{'='*60}\n")

    # Show the top 5 cheapest flights
    for i, flight in enumerate(results[:5], 1):
        print(f"{i}. {flight.airlines}")
        print(f"   Price: ${flight.price}")
        print(f"   Duration: {flight.duration}")
        print(f"   Stops: {flight.stops}")
        print()

What does this code do?

  • if __name__ == "__main__": ensures this block only runs when you execute the file directly (not when it's imported by another script later).
  • search_route("JFK", "LHR", "2026-09-15") searches for flights from New York JFK to London Heathrow on September 15, 2026.
  • results[:5] limits the output to the 5 cheapest flights so you get a focused view without flooding the terminal.
  • Each flight's airlines, price, duration, and stops are printed in a numbered list.
  • Save search_flights.py.

Before you run this, what do you expect to see? Will you get flight data, or might something go wrong?

  • Run your script to fetch live flight prices:
python3 search_flights.py

You should see a numbered list of flights with airline names, dollar prices, flight duration, and the number of stops for each result.

What should I see?

Your output will look something like this (exact values depend on current prices):

============================================================
Flights from JFK to LHR on 2026-09-15
============================================================

1. ['Norse Atlantic Airways']
   Price: $289
   Duration: 7h 15m
   Stops: 0

2. ['United']
   Price: $342
   Duration: 6h 50m
   Stops: 0

The prices, airlines, and times will differ from this example because they reflect live Google Flights data at the moment you run the script.

Not seeing any results?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, run source venv/bin/activate again.
  • Check that the date in your code is in the future. Google Flights does not return results for past dates.
  • If you see a connection error, confirm you have an active internet connection. The library makes a live request to Google's servers.

Still stuck? Why isn't my flight search returning results?

✔️ Awesome, I've got everything!

Great. Double check you've saved search_flights.py and your output shows airline names, prices, durations, and stops.

ⓧ I'd like to double check the full code

Your search_flights.py should contain both chunks you wrote above, in order:

  • The imports and search_route() function (the first code block you added).
  • The if __name__ == "__main__": block (the second code block you added).

Scroll up and confirm each section is present and in order. Make sure there is one blank line between the end of the search_route() function and the if __name__ block.

You just pulled live flight prices from Google Flights with a few lines of Python. Next up, you'll build a multi-route watchlist so the system can monitor several trips at once and flag the ones worth buying.

Build a Multi-Route Watchlist

In the previous step, you proved you can pull live flight prices for a single route. That is useful for a one-off check, but it means you have to manually run the script for every route you care about.

A real alert system monitors multiple routes at once, each with its own budget threshold. In this step, you will create a watchlist of routes and build a checker that loops through them all, comparing each cheapest price against your target.

In this step, get ready to:
  • Define a multi-route watchlist with target prices per route.
  • Build a deal-checking script that searches all routes and flags deals.
  • Run the checker and see which routes have deals below your budget.
Create your route watchlist

The watchlist is a simple Python list where each entry defines one flight route you want to monitor. Each route needs an origin airport, a destination airport, a travel date, and a maximum price you are willing to pay.

  • Create a new file called watchlist.py in your flight-alerts folder and add the following code:
WATCHLIST = [
    {
        "origin": "JFK",
        "destination": "LHR",
        "date": "2026-09-15",
        "max_price": 450,
    },
    {
        "origin": "LAX",
        "destination": "NRT",
        "date": "2026-10-01",
        "max_price": 600,
    },
    {
        "origin": "SFO",
        "destination": "CDG",
        "date": "2026-09-20",
        "max_price": 500,
    },
]

What does this code do?

  • WATCHLIST is a list of dictionaries. Each dictionary represents one route to monitor.
  • origin and destination use 3-letter IATA airport codes (e.g. JFK for New York, LHR for London Heathrow, NRT for Tokyo Narita, CDG for Paris).
  • max_price is your deal threshold in USD. Any flight at or below this price counts as a deal.
  • Save watchlist.py.
  • Verify the file imports correctly by running this command:
python3 -c "from watchlist import WATCHLIST; print(len(WATCHLIST), 'routes loaded')"

You should see 3 routes loaded printed in your terminal.

Seeing a ModuleNotFoundError?

  • Make sure you are running the command from inside your flight-alerts folder. Python looks for modules in the current directory.
  • Check that the file is saved as watchlist.py (not watchlist.txt or similar).

Why can't Python find my watchlist module?

Customise your routes

Feel free to swap the example routes for airports you actually care about. Use any 3-letter IATA airport code. You can find yours by searching your city name on Google Flights and noting the code in parentheses.

Build the deal checker

Now you need a script that loops through every route in your watchlist, searches for the cheapest flight, and compares it against your target price. This is the script you will eventually run on a schedule.

  • Create a new file called check_deals.py in your flight-alerts folder and add the following code to set up the loop:
from watchlist import WATCHLIST
from search_flights import search_route
from datetime import datetime


def check_all_routes():
    print(f"\nChecking {len(WATCHLIST)} routes at {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
    deals_found = []

    for route in WATCHLIST:
        origin = route["origin"]
        destination = route["destination"]
        date = route["date"]
        max_price = route["max_price"]

        print(f"Searching {origin} -> {destination} on {date}...")

    print(f"\n{len(deals_found)} deal(s) found.")


if __name__ == "__main__":
    check_all_routes()

What does this code do?

  • The function imports your WATCHLIST and the search_route() function you built in the previous step.
  • It loops through each route dictionary, extracting the airport codes, date, and budget.
  • deals_found will collect any flights that beat your target price. Right now the loop just prints each route being checked.
  • Save check_deals.py and run it to confirm the loop iterates through all routes:
python3 check_deals.py

You should see each route printed as "Searching JFK -> LHR..." and a summary of "0 deal(s) found" at the end.

Seeing an ImportError?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • Check that watchlist.py and search_flights.py are in the same folder as check_deals.py.

Why is Python unable to import my modules?

The loop works, but it is not actually searching for flights yet. Now you will add the logic that calls search_route() for each route and compares the cheapest price against your budget.

  • In check_deals.py, find the line that says print(f"Searching {origin} -> {destination} on {date}...") and add the following code directly below it, still inside the for loop:
        try:
            results = search_route(origin, destination, date)

            if not results:
                print(f"  No results found.")
                continue

            cheapest = results[0]
            price = cheapest.price

            if price <= max_price:
                print(f"  DEAL FOUND! ${price} (target: ${max_price})")
                deals_found.append({
                    "origin": origin,
                    "destination": destination,
                    "date": date,
                    "airline": ", ".join(cheapest.airlines) if isinstance(cheapest.airlines, list) else str(cheapest.airlines),
                    "price": price,
                    "max_price": max_price,
                    "duration": str(cheapest.duration),
                    "stops": cheapest.stops,
                })
            else:
                print(f"  Cheapest: ${price} (target: ${max_price}) - no deal")

        except Exception as e:
            print(f"  Error searching route: {e}")

What does this code do?

  • search_route() calls Google Flights for each route. The results come back sorted by price (cheapest first).
  • results[0] grabs the cheapest flight. If its price is at or below your max_price, the script prints "DEAL FOUND" and saves the flight details.
  • The try/except block catches network errors or unexpected responses so one failed route does not crash the entire checker.
  • Save check_deals.py.

Before you run it, think about what you expect to see. Will all three routes show deals, or will some be above your target price?

  • Run the deal checker:
python3 check_deals.py

You should see each route searched with either "DEAL FOUND" or "no deal" printed next to the cheapest price and your target. The summary at the end tells you how many deals were found across all routes.

Not seeing any deals?

If all routes show "no deal", try raising the max_price for one route in watchlist.py to something very high (like 9999) and run again. This confirms the "DEAL FOUND" path works. You can set it back to a realistic target afterwards.

Seeing connection or timeout errors?

  • The faster-flights library scrapes Google Flights, which can occasionally rate-limit requests. Wait a minute and try again.
  • Make sure you have an active internet connection.
  • If one route fails but others succeed, the try/except will print the error and continue. This is expected behaviour.

Why is my flight search timing out or returning errors?

✔️ Awesome, I've got everything!

Great. Double check you have saved both watchlist.py and check_deals.py.

ⓧ I'd like to double check the full code

Here is the complete watchlist.py:

WATCHLIST = [
    {
        "origin": "JFK",
        "destination": "LHR",
        "date": "2026-09-15",
        "max_price": 450,
    },
    {
        "origin": "LAX",
        "destination": "NRT",
        "date": "2026-10-01",
        "max_price": 600,
    },
    {
        "origin": "SFO",
        "destination": "CDG",
        "date": "2026-09-20",
        "max_price": 500,
    },
]

For check_deals.py, confirm you have both chunks in order:

  • The imports, check_all_routes() function definition, the for loop extracting route variables, and the print(f"Searching...") line.
  • The try/except block with search_route(), price comparison, deals_found.append(), and the error handler.
  • After the for loop (dedented): print(f"\n{len(deals_found)} deal(s) found.") and the if __name__ == "__main__" block calling check_all_routes().

Your flight checker now scans multiple routes and tells you which ones have deals. Next up, you will wire in email notifications so you do not have to watch the terminal output yourself.

Send Email Alerts

Your deal checker prints results to the terminal, but that only helps if you happen to be watching. The real power of an alert system is that it reaches out to YOU when something worth acting on appears.

In this step, you will wire up email notifications using Python's built-in smtplib module and Gmail's SMTP server. When a flight drops below your target price, your script will send you a formatted email with all the details you need to book fast.

In this step, get ready to:
  • Set up email credentials securely using environment variables.
  • Build a notification module that formats and sends deal emails.
  • Wire notifications into your deal checker so emails fire automatically.
Set up email credentials

Your script needs a Gmail address and an app password to send emails programmatically. An app password is a 16-digit code that lets your Python script access Gmail without exposing your real password.

What is a Gmail app password?

Gmail blocks direct username/password logins from scripts for security reasons. Instead, you generate a special 16-character app password from your Google Account that works only for this one connection.

To create one: enable 2-Step Verification on your Google Account, then visit myaccount.google.com/apppasswords to generate a new app password. Copy the 16-character code. You will paste it into your .env file next.

  • Install python-dotenv so your script can load credentials from a .env file by running:
pip install python-dotenv==1.1.0
  • Create a .env file in your flight-alerts folder with your email credentials by running:
touch .env
  • Open .env in your editor and paste the following, replacing the placeholder values with your real Gmail address and app password:
SENDER_EMAIL=your-email@gmail.com
SENDER_PASSWORD=your-app-password-here
RECIPIENT_EMAIL=your-email@gmail.com

What do these variables mean?

  • SENDER_EMAIL is the Gmail address your script sends FROM.
  • SENDER_PASSWORD is the 16-character app password you generated (not your regular Gmail password).
  • RECIPIENT_EMAIL is where the alerts land. It can be the same address or a different one.
  • Create a .env.example file as a template (safe to commit to version control) by running:
touch .env.example
  • Open .env.example and paste the same structure with placeholder values:
SENDER_EMAIL=your-email@gmail.com
SENDER_PASSWORD=your-app-password-here
RECIPIENT_EMAIL=your-email@gmail.com
  • Confirm both files exist by running:
ls -a .env*

You should see .env and .env.example in the output.

Don't see the App Passwords option in Google?

You must have 2-Step Verification enabled on your Google Account before the App Passwords page appears. If you are on a work or school account, your administrator may have disabled app passwords.

help me set up a Gmail app password for my Python script

Create the notification module

The notification module handles building a formatted email from your deal results and sending it through Gmail's SMTP server. You will split this into its own file so the logic stays reusable.

  • Create notify.py and add the imports and credential loading at the top:
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

from dotenv import load_dotenv

# Load credentials from .env file
load_dotenv()

# Read email settings into variables
SENDER_EMAIL = os.getenv("SENDER_EMAIL")
SENDER_PASSWORD = os.getenv("SENDER_PASSWORD")
RECIPIENT_EMAIL = os.getenv("RECIPIENT_EMAIL")

What does this code do?

  • load_dotenv() reads your .env file and makes its values available through os.getenv().
  • smtplib is Python's built-in module for sending emails via SMTP.
  • MIMEText and MIMEMultipart format the email with proper headers so mail servers accept it.
  • Verify the imports work by running:
python3 -c "from notify import SENDER_EMAIL; print('Notify module loaded!')"

You should see Notify module loaded! printed.

Seeing a ModuleNotFoundError?

If you see an error mentioning dotenv, confirm your virtual environment is active (you should see (venv) in your terminal prompt). Then re-run pip install python-dotenv==1.1.0.

help me fix a ModuleNotFoundError for dotenv in my notify.py

  • Now add the send_alert function below the credential variables in notify.py:
def send_alert(deals):
    if not deals:
        return

    # Build the email structure
    msg = MIMEMultipart()
    msg["From"] = SENDER_EMAIL
    msg["To"] = RECIPIENT_EMAIL
    msg["Subject"] = f"Flight Deal Alert! {len(deals)} deal(s) found"

    # Format each deal into readable lines
    body_lines = ["Cheap flight deals found:\n"]

    for deal in deals:
        body_lines.append(f"Route: {deal['origin']} -> {deal['destination']}")
        body_lines.append(f"Date: {deal['date']}")
        body_lines.append(f"Airline: {deal['airline']}")
        body_lines.append(f"Price: ${deal['price']} (target: ${deal['max_price']})")
        body_lines.append(f"Duration: {deal['duration']}")
        body_lines.append(f"Stops: {deal['stops']}")
        body_lines.append(f"Savings: ${deal['max_price'] - deal['price']} below target")
        body_lines.append("-" * 40)

What does this code do?

  • The function takes a list of deal dictionaries (the same ones check_deals.py already builds).
  • It constructs a multipart email with a clear subject line showing how many deals were found.
  • Each deal gets formatted with route, airline, price, duration, stops, and savings below target.
  • Still in the send_alert function, add the code that joins the body lines and sends the email. Paste this directly below the body_lines.append("-" * 40) line, at the same indentation level as the for loop:
    # Join all lines and attach as plain text
    body = "\n".join(body_lines)
    msg.attach(MIMEText(body, "plain"))

    # Connect to Gmail's SMTP server and send
    with smtplib.SMTP("smtp.gmail.com", 587) as server:
        server.starttls()
        server.login(SENDER_EMAIL, SENDER_PASSWORD)
        server.sendmail(SENDER_EMAIL, RECIPIENT_EMAIL, msg.as_string())

    print(f"Alert email sent to {RECIPIENT_EMAIL}!")

What does this code do?

  • smtp.gmail.com on port 587 is Gmail's SMTP server with STARTTLS encryption.
  • starttls() upgrades the connection to encrypted before sending credentials.
  • The with block ensures the connection closes cleanly even if something fails.
  • Save notify.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved notify.py with both the imports/credentials section and the complete send_alert function.

ⓧ I'd like to double check the full code

Your notify.py should have these sections in order:

  • Imports (os, smtplib, MIMEText, MIMEMultipart, load_dotenv).
  • load_dotenv() call followed by three os.getenv() assignments.
  • send_alert(deals) function that builds a multipart email, loops through deals to format body lines, joins them, attaches the body, connects to smtp.gmail.com:587, and sends.

Scroll up and confirm each chunk matches what you pasted.

Wire notifications into check_deals.py

The notification module is ready, but check_deals.py does not call it yet. You need to import send_alert and trigger it when deals are found.

  • In check_deals.py, find the imports at the top of the file:
from watchlist import WATCHLIST
from search_flights import search_route
from datetime import datetime
  • Replace those three lines with the following updated imports:
from datetime import datetime

from watchlist import WATCHLIST
from search_flights import search_route
from notify import send_alert
  • Now scroll to the bottom of the check_all_routes() function and find this line:
    print(f"\n{len(deals_found)} deal(s) found.")
  • Replace that line with the following, which sends an email when deals exist and prints a different message when none are found:
    if deals_found:
        send_alert(deals_found)
        print(f"\n{len(deals_found)} deal(s) found and emailed!")
    else:
        print("\nNo deals found this time.")

What changed?

Instead of always printing the count, the script now checks whether any deals were found. If yes, it calls send_alert(deals_found) to email you before printing the count. If no deals matched, it prints a short message so you know the check ran successfully.

  • Save check_deals.py.

Guarantee a deal fires for testing

To make sure at least one route triggers a deal, temporarily set a high max_price in watchlist.py (e.g., 9999). This ensures the cheapest flight will always be below your target. Remember to set it back to a realistic number after testing.

Before you run the script, what do you think will happen? Will you see terminal output, get an email, or both?

  • Run the deal checker by executing:
python3 check_deals.py

You should see the route searches in the terminal, a "DEAL FOUND" message for any route below your target, and the line Alert email sent to followed by your email address. Check your inbox (and spam folder) for the email with the subject "Flight Deal Alert!" containing the formatted deal details.

Not receiving the email?

  • Check that your .env file has the correct Gmail address and the 16-character app password with no extra spaces.
  • Make sure 2-Step Verification is enabled on your Google Account. App passwords only work with 2-Step Verification active.
  • Check your spam folder. The first email from a new SMTP sender often lands there.

help me debug why my Python script isn't sending emails through Gmail SMTP

Your alert system now sends real emails when flight prices drop below your targets. Next up, you will schedule this script to run automatically so you never have to remember to check prices yourself.

Schedule Automatic Checks

Your flight alert system works perfectly when you remember to run it. But the whole point of an alert system is that it works without you. Right now, you would have to manually type python3 check_deals.py every time you want a price check.

In this step, you will add logging so the script records what it did each time it runs. Then you will schedule it with cron to run automatically twice a day. This turns it from a manual tool into a true "set and forget" alert system.

In this step, get ready to:
  • Add logging to record what happens on each run.
  • Schedule the script with a cron job to run automatically.
  • Verify the automated run fires and writes to the log.
Add logging to record each run

When the script runs unattended, you will not be watching the terminal. You need a log file that records what happened on each run so you can check later whether things are working.

  • In check_deals.py, add import logging as the very first line (above the existing from datetime import datetime line).
  • Then add a logging.basicConfig() block after all the import statements (after from notify import send_alert).
  • Here is what the top of your file should now look like:
import logging
from datetime import datetime

from watchlist import WATCHLIST
from search_flights import search_route
from notify import send_alert

logging.basicConfig(
    filename="flight_alerts.log",
    level=logging.INFO,
    format="%(asctime)s - %(message)s",
)

What does this code do?

logging.basicConfig() configures Python's built-in logging module to write messages to a file instead of the terminal.

  • filename="flight_alerts.log" tells it to write to a file called flight_alerts.log in your project folder.
  • level=logging.INFO means it will capture informational messages (not just errors).
  • format="%(asctime)s - %(message)s" stamps every entry with the date and time so you know when each run happened.

Now you need to add two logging calls inside the check_all_routes() function so it actually writes entries to the log.

  • Find the except Exception as e: block inside the for route in WATCHLIST: loop. Currently it only has a print statement. Add this line directly below the existing print:
            logging.error(f"Error searching {origin}->{destination}: {e}")
  • Next, scroll down to the very end of the check_all_routes() function. After the if deals_found / else block that prints the summary, add this line (at the same indentation level as the if deals_found: statement):
    logging.info(f"Checked {len(WATCHLIST)} routes, found {len(deals_found)} deals")

What do these logging calls do?

logging.error() records any route that failed to search (network issues, invalid dates, etc.) so you can spot problems.

logging.info() writes a summary line after every run. When you check your log file tomorrow, you will see entries like 2026-07-25 08:00:01 - Checked 3 routes, found 1 deals confirming the script ran.

This is how you prove the scheduled job is working without needing to watch the terminal.

  • Save check_deals.py.

Before you think about scheduling, what do you expect will happen when you run the script now? Will the log file appear?

  • Run the script to test the logging by running:
python3 check_deals.py
  • After the script finishes, check for the log file by running:
cat flight_alerts.log

You should see a timestamped entry that says how many routes were checked and how many deals were found.

Don't see the log file?

  • Make sure you saved check_deals.py after adding the logging code.
  • Check that logging.basicConfig(...) is at the top level of the file (not indented inside a function).
  • Confirm the logging.info(...) line is inside check_all_routes() but outside the for loop (same indent as if deals_found:).

Why isn't my flight_alerts.log file appearing after I run the script?

✔️ Awesome, I've got everything!

Great. Make sure you have saved check_deals.py and confirmed the log file was created.

ⓧ I'd like to double check the full code

Scroll up and confirm these additions are all present in check_deals.py, in order:

  • import logging as the very first line.
  • logging.basicConfig(...) block after all imports, before the function definition.
  • logging.error(...) inside the except block, after the existing print statement.
  • logging.info(...) at the end of check_all_routes(), after the if deals_found / else block.
Set up a cron job for automatic scheduling

Cron is a built-in scheduler on macOS and Linux that runs commands at times you define. You give it a schedule (like "every day at 8am") and a command, and it handles the rest.

  • First, you need the full file paths that cron will use. Find the path to your virtual environment's Python by running:
which python3

You should see something like /Users/yourname/Desktop/flight-alerts/venv/bin/python3. Copy this path. You will need it in a moment.

  • Now find the full path to your project folder by running:
pwd

You should see something like /Users/yourname/Desktop/flight-alerts. Copy this too.

Why do you need full paths?

Cron runs commands in a minimal environment with no knowledge of your current directory or virtual environment. It needs the absolute path to both the Python interpreter (inside your venv) and the script file.

  • Open the cron editor by running:
crontab -e

What will you see?

This opens vim (a terminal text editor) by default on macOS. If you have not used vim before, follow these steps carefully:

  • Press i to enter insert mode (you can now type).
  • Type or paste your cron line (shown below).
  • Press Escape to exit insert mode.
  • Type :wq and press Enter to save and quit.

For testing, set the cron job to fire 2 minutes from now so you can verify it works. Check the current time by glancing at your clock, then calculate 2 minutes ahead.

  • In the vim editor, press i then type a line in this format (replacing the paths with your own and the time with 2 minutes from now):
[[CRON_MINUTE="minute (e.g. 17)"]] [[CRON_HOUR="hour in 24h format (e.g. 10)"]] * * * [[VENV_PYTHON_PATH="/full/path/to/flight-alerts/venv/bin/python3"]] [[PROJECT_PATH="/full/path/to/flight-alerts"]]/check_deals.py

How does the cron schedule work?

The five fields before the command are: minute, hour, day-of-month, month, day-of-week. An asterisk (*) means "every."

So 17 10 * * * means "at 10:17 every day." You are using a specific time 2 minutes from now just to test that it fires.

  • Press Escape, type :wq, and press Enter to save and exit.

You should see a message like crontab: installing new crontab confirming it was saved.

Seeing a permission error on macOS?

macOS may prompt you to allow cron access in System Settings > Privacy & Security > Full Disk Access. Add your Terminal app (or iTerm) to the list, then try crontab -e again.

Why is cron being blocked by macOS privacy settings?

Now wait 2 minutes for the cron job to fire. What do you expect to find in the log file once it runs?

  • After 2 minutes have passed, check the log file by running:
cat flight_alerts.log

You should see a new timestamped entry at the bottom of the log. This proves cron successfully ran your script without any manual intervention.

No new log entry after 2 minutes?

  • Double-check that your cron line uses the full absolute path to the venv Python and the script. Run crontab -l to view what is saved.
  • Make sure the virtual environment Python path points into venv/bin/python3, not your system Python.
  • Confirm the minute and hour in your cron entry have already passed by checking your clock again.

My cron job didn't run. How do I debug what went wrong?

Now that you have confirmed it works, update the cron job to the real schedule.

  • Open the cron editor again by running:
crontab -e
  • Press i to enter insert mode. Replace the test time with 0 8,20 so the line looks like this:
0 8,20 * * * [[VENV_PYTHON_PATH="/full/path/to/flight-alerts/venv/bin/python3"]] [[PROJECT_PATH="/full/path/to/flight-alerts"]]/check_deals.py
  • Press Escape, type :wq, and press Enter to save.

What does 0 8,20 mean?

0 8,20 * * * means "at minute 0 of hours 8 and 20, every day." Your script will now automatically check flight prices at 8:00 AM and 8:00 PM daily.

  • Verify your final cron entry is saved correctly by running:
crontab -l

You should see the line with 0 8,20 * * * followed by the full paths to your Python interpreter and script.

Your flight alert system is now fully automated. Every morning at 8 AM and every evening at 8 PM, your script silently checks all your watched routes, emails you if a deal appears, and logs what it did. Next up, the Secret Mission challenges you to track price history and spot trends over time.

Secret mission

Track Price History

Knowing today's price is useful, but knowing whether it's above or below the recent average is what separates a good deal from a great one. Add a price history tracker that saves every price check to a JSON file and calculates a 7-day rolling average, so your alerts only fire when a flight is genuinely cheap compared to recent trends.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with no cloud resources or paid APIs, so there are no ongoing costs.

Resources you used:

  • A cron job running check_deals.py twice daily on your machine.
  • The flight-alerts project folder on your Desktop (includes virtual environment, credentials in .env, price history data, and log files).

Keep everything running

No action needed. Choose this if you want to keep receiving flight deal alerts automatically.

  • Your cron job continues running twice daily and will email you when deals appear.
  • Your price_history.json file grows over time, giving you better rolling averages for each route.
  • You can add new routes to watchlist.py anytime without changing anything else.

Pause - I'll come back to this later

Disable the automated checks without losing any of your project files or price history.

  • Open your crontab by running:
crontab -e
  • Find the line that runs check_deals.py and add a # at the beginning of the line to comment it out.
  • Save and close the file. The cron job is now paused.
  • To resume later, open crontab -e again and remove the # prefix.

Delete - I don't want to use this again

Remove the cron job and delete the entire project from your machine.

  • Remove the cron job by running:
crontab -e
  • Delete the entire line that runs check_deals.py, then save and close the file.
  • Deactivate the virtual environment (if still active) by running:
deactivate
  • Delete the project folder by running:
rm -rf ~/Desktop/flight-alerts
  • On Windows, use this command instead:
rmdir /s /q %USERPROFILE%\Desktop\flight-alerts
  • This removes all Python files, the virtual environment, your .env credentials, price history, and log files.
  • If you used a Gmail app password solely for this project, you can revoke it in your Google Account under Security > App passwords.

Nice Work!

Nice Work!

You just built a personal flight deal hunter that monitors real Google Flights prices and emails you the moment a route drops below your threshold. No paid APIs, no manual checking, just a Python script working quietly in the background.

Here's what you learned:

  • Pulled live flight pricing data from Google Flights using the faster-flights library, with results sorted by price and filtered by route.
  • Built a multi-route watchlist with configurable target prices, and wrote comparison logic that identifies genuine deals across all your watched routes.
  • Automated email notifications using Python's built-in smtplib and scheduled the whole system to run hands-off with cron.
  • Secret Mission: Added price history tracking with JSON storage and a 7-day rolling average, so alerts only fire when a price is both below your target and below the recent trend.

Ready to quiz yourself?