Build an AI Python File Summarizer

Build a Python CLI that summarizes local text files with Gemini AI.

Introduction

30 Second Summary

Long documents often land in front of you when all you need is the main point. Copying them into another app adds friction when the source already lives on your Mac.

In this project, you will build TextBrief, a Python command-line tool that summarizes local text files through the Gemini Developer API. You will choose a concise paragraph or bullet points while clear errors handle bad input.

What You'll Build

You run TextBrief against sample.txt to see a concise summary in Terminal with your choice of paragraph or bullet format.

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

  • A concise terminal summary that reduces a local UTF-8 text file to its main ideas.
  • Selectable output styles that show the same source as one short paragraph or three to five scannable bullet points.
  • Production-style safeguards covering an API key stored in the GEMINI_API_KEY environment variable plus one-line errors for missing, empty, or invalid files.
  • Secret Mission: Add an optional output path so TextBrief can save each summary as a reusable UTF-8 text file.

Are there any prerequisites?

You need basic Python syntax, a Mac running Python 3.10+, an installed code editor, and a Google account for Google AI Studio.

The free tier is suitable only for non-sensitive text because content may be used to improve Google products.

Before We Start

This step locks in what you want TextBrief to summarize before the hands-on work begins. Choose a non-sensitive UTF-8 text use case that will benefit from a terminal-based CLI.

Set Up Gemini and Your Python Project

TextBrief sends local text to the Gemini Developer API. An unsupported Python version can stop the SDK before your CLI runs.

A clean virtual environment isolates the project dependencies. A smoke test proves your credential works before you build the summarizer.

In this step, get ready to:
  • Confirm that Python 3.10 or newer is available.
  • Create an isolated TextBrief environment with the pinned SDK.
  • Create Gemini API credentials for a successful smoke test.
Prepare the Python environment

The Google Gen AI SDK requires Python 3.10 or newer. Checking your version first prevents dependency problems during installation.

  • Press Cmd+Space to open Spotlight search.
  • Type Terminal into Spotlight search.
  • Press Enter to open Terminal.
  • Check your installed Python version by running this command:
python3 --version

What does this command do?

The command asks Python to report its installed version. The result determines whether your interpreter can run the pinned SDK.

✔️ I see Python 3.10 or higher

Your Python version meets the SDK requirement. You can continue with the project environment below.

ⓧ I see an older version

Your current interpreter is below the required version. Install a current Python 3 release before creating the virtual environment.

  • Download the latest stable macOS installer from the Python.org macOS downloads page.
  • Open the downloaded installer.
  • Keep the default selections throughout the installation wizard.
  • Close Terminal after the installation finishes.
  • Press Cmd+Space to reopen Spotlight search.
  • Type Terminal into Spotlight search.
  • Press Enter to reopen Terminal.
  • Check the new Python version by running this command:
python3 --version

What should the version check show?

The result needs to show Python 3.10 or newer. Keep this Terminal window open once the version meets that requirement.

ⓧ Command not found

Terminal cannot find a Python 3 installation. The official macOS installer adds the interpreter needed for this project.

  • Download the latest stable macOS installer from the Python.org macOS downloads page.
  • Open the downloaded installer.
  • Keep the default selections throughout the installation wizard.
  • Close Terminal after the installation finishes.
  • Press Cmd+Space to reopen Spotlight search.
  • Type Terminal into Spotlight search.
  • Press Enter to reopen Terminal.
  • Confirm Python is available by running this command:
python3 --version

What should the version check show?

The result needs to show Python 3.10 or newer. Keep this Terminal window open once the version meets that requirement.

  • Move to your Desktop by running this command:
cd ~/Desktop

Why start on the Desktop?

This command gives the project a predictable location. You can find the textbrief folder from Finder or Terminal later.

  • Create the textbrief folder and enter it by running these commands:
mkdir textbrief
cd textbrief

What do these commands do?

  • The first command creates the textbrief folder on your Desktop.
  • The second command makes textbrief the active Terminal folder.

Your Terminal prompt now points to the textbrief folder. Every project file you create next belongs here.

  • Create the isolated environment and activate it by running these commands:
python3 -m venv .venv
source .venv/bin/activate

What do these commands do?

  • The first command creates a project-specific Python environment inside .venv.
  • The second command activates that environment for the current shell session.

Your Terminal prompt now includes the environment name. This confirms that future package installations stay inside TextBrief.

Virtual environment not active?

Confirm that Terminal is inside the textbrief folder. Rerun the activation command from that folder.

Still stuck? Help me activate the TextBrief virtual environment on macOS.

Install the pinned SDK

A dependency pin keeps every learner on the same SDK release. The requirements.txt file records the exact package version for repeatable installs.

  • Use your code editor's new-file control to create requirements.txt inside ~/Desktop/textbrief.
  • Add the pinned dependency to requirements.txt using this content:
google-genai==2.33.0

What does this dependency pin do?

The line installs the Google Gen AI SDK at version 2.33.0. Pinning the release keeps the API behavior consistent throughout the project.

  • Save requirements.txt.
  • Confirm the file exists in textbrief by running this command:
ls

What does this check prove?

The command lists the visible files in the current folder. It confirms that your editor saved requirements.txt in the Terminal folder.

You should see requirements.txt in the output.

Requirements file missing?

Check that your code editor saved the file inside ~/Desktop/textbrief. A file saved elsewhere does not appear in this listing.

Need help locating it? Help me find where my requirements.txt file was saved.

✔️ Awesome, I've got everything!

Your dependency file is saved in the correct folder. Keep the activated Terminal session open for installation.

ⓧ I'd like to double check the full code

google-genai==2.33.0

How to use this reference

Compare this reference with your saved requirements.txt file. The package name and version pin must match exactly.

  • Install the pinned project dependency by running this command:
python3 -m pip install -r requirements.txt

What does this command do?

Python asks pip to read the dependency from requirements.txt. The package is installed inside the active virtual environment.

The terminal finishes with installation output for google-genai. Your TextBrief environment can now import the SDK.

SDK installation failed?

Confirm that your Terminal prompt still shows the active virtual environment. Check that requirements.txt contains the exact dependency pin from the reference tab.

Still having trouble? Help me troubleshoot the Google Gen AI SDK installation.

Create the credential and run a smoke test

Creating an API key is the sensitive part of this setup. You keep it in the current shell without placing it in a Python file.

Use non-sensitive text

Gemini 3.8 Flash input and output are free of charge on the free tier. Google may use free-tier content to improve its products.

Use only non-sensitive text files in this project. The synthetic sample in the next step is safe for your first summary.

  • Open the official Gemini API key guide in your browser.
  • Select Create or view a Gemini API Key near the top of the guide.
  • Sign in to Google AI Studio with your Google account.
  • Select Create API key.
  • Copy the generated key.
  • Return to the Terminal window from earlier.
  • Replace <YOUR_API_KEY_HERE> with the copied key before running this command:
export GEMINI_API_KEY=<YOUR_API_KEY_HERE>

How does the key stay secure?

The command stores GEMINI_API_KEY in the current zsh session. The SDK detects that environment variable when it creates a client.

Closing this Terminal session removes the export. Never paste the key into a Python file or commit it to source control.

  • Use your code editor's new-file control to create smoke_test.py inside ~/Desktop/textbrief.
  • Add the smoke test to smoke_test.py using this code:
from google import genai


client = genai.Client()
try:
    interaction = client.interactions.create(
        model="gemini-3.8-flash",
        input="Reply with exactly: Gemini is ready.",
    )
    print(interaction.output_text)
finally:
    client.close()

What does this code do?

  • The import makes the installed Gemini SDK available to the script.
  • The genai.Client() call creates a client using the key from the environment.
  • The client.interactions.create() call asks gemini-3.8-flash for a precise test response.
  • The finally block closes the client even if the request fails.
  • Save smoke_test.py.

✔️ Awesome, I've got everything!

Your smoke test is saved. Keep your current Terminal session open because it holds the exported API key.

ⓧ I'd like to double check the full code

from google import genai


client = genai.Client()
try:
    interaction = client.interactions.create(
        model="gemini-3.8-flash",
        input="Reply with exactly: Gemini is ready.",
    )
    print(interaction.output_text)
finally:
    client.close()

How to use this reference

Compare this full reference with your saved smoke_test.py file. Keep the model ID and test prompt exactly as shown.

Before you run the smoke test, do you expect the SDK to reach Gemini with the key stored in this Terminal session?

  • Run the smoke test from the textbrief folder with this command:
python3 smoke_test.py

What does this command prove?

This command runs the saved Python script inside your active environment. A successful response proves that the SDK can authenticate with Gemini.

You should see a short response containing Gemini is ready. in the terminal. That response confirms your dependency and credential are working.

Smoke test not responding?

  • Confirm that the virtual environment is still active.
  • Confirm that you exported GEMINI_API_KEY in this same Terminal window.
  • Compare smoke_test.py with the full-code reference tab.

Need a second pair of eyes? Help me diagnose why the TextBrief smoke test cannot reach Gemini.

That first API response is your setup milestone. Next, you will turn the same connection into a CLI that summarizes a real text file.

Summarize Your First Text File

Your smoke test proved that the Gemini Developer API responds through your installed SDK. Your credentials are ready for a real task.

A useful summarizer needs to read a local UTF-8 text file before it can send the contents to Gemini. In this step, you will connect file reading to your first working command-line interface.

In this step, get ready to:
  • Create a predictable sample article with no sensitive information.
  • Build a Python CLI that accepts a local file path.
  • Send the file contents to Gemini for a concise summary.
Create a predictable sample article

A fixed sample makes your first result easier to evaluate. This article is synthetic, so it keeps sensitive information away from the API.

  • Press Cmd+Space to open Spotlight on your Mac.
  • Type the name of your installed code editor into Spotlight.
  • Press Enter to open your code editor.
  • Use your editor's folder-opening control from its main menu.
  • Select the textbrief folder that is already open in Terminal.
  • Use the new-file control in your editor's file sidebar.
  • Name the new file sample.txt.
  • Add the sample article by copying this text into sample.txt:
Neighborhood libraries are expanding beyond book lending. Many now provide tool libraries, recording studios, coding workshops, and quiet workspaces for residents who may not have these resources at home.

These services help libraries respond to changing community needs while preserving their traditional role as free places to learn. Staff members say the strongest programs are designed with direct input from local residents.

What Is in This Sample?

  • The first paragraph describes new services offered by neighborhood libraries.
  • The second paragraph explains how community input shapes those services.
  • The two-paragraph structure gives Gemini enough context to identify the central idea.
  • Save sample.txt.
  • Confirm that your editor shows two paragraphs about neighborhood libraries.

Sample Text Missing or Incomplete?

Check that sample.txt sits directly inside the textbrief folder. Confirm that both paragraphs were copied before you saved the file.

Still stuck? Help me check why my sample.txt file is missing or incomplete.

Build the file-reading CLI

The CLI needs a positional argument for the file path. Python's Path type converts that argument into an object that can read the file as text.

  • Return to the code editor from earlier.
  • Use the new-file control in the editor's file sidebar.
  • Name the new file summarize.py.
  • Add the CLI foundation by copying this code into summarize.py:
import argparse
import os
from pathlib import Path

from google import genai


MODEL_ID = "gemini-3.8-flash"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Summarize a UTF-8 text file with Gemini."
    )
    parser.add_argument(
        "file",
        type=Path,
        help="Path to the UTF-8 text file to summarize.",
    )
    return parser.parse_args()

What Does This Code Do?

  • The imports provide command-line parsing, environment variable access, filesystem paths, and the Gemini client.
  • The MODEL_ID constant keeps the selected Gemini model in one place.
  • The parse_args() function defines one required file argument.
  • The type=Path setting converts the typed path before the rest of the program uses it.
  • Save summarize.py.
  • Confirm that your editor's file sidebar now lists summarize.py beside sample.txt.

Cannot Find summarize.py?

Confirm that your editor has the textbrief folder open. Check that the filename ends with .py.

Need another pair of eyes? Help me verify where summarize.py was created.

Send the article to Gemini

The remaining functions read the article and turn it into a focused prompt. The main function then sends that prompt through the same client pattern you verified in the smoke test.

  • Place your cursor after the return parser.parse_args() line in summarize.py.
  • Add the file-reading and API logic by copying this code below that line:


def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def build_prompt(text: str) -> str:
    return f"Summarize the text in one concise paragraph of no more than 100 words.\n\nText to summarize:\n{text}"


def main() -> None:
    args = parse_args()
    text = read_text(args.file)

    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text),
        )
        print(interaction.output_text)
    finally:
        client.close()


if __name__ == "__main__":
    main()

How Does the Summary Flow Work?

  • The read_text() function decodes the selected file as UTF-8 text.
  • The build_prompt() function asks for one concise paragraph of no more than 100 words.
  • The environment check stops the request when GEMINI_API_KEY is unavailable.
  • The main() function sends the prompt to Gemini before printing the returned text.
  • The finally block closes the client after the request finishes.
  • Save summarize.py.

✔️ Awesome, I've got everything!

Your saved summarize.py now contains the complete first build of TextBrief.

ⓧ I'd like to double check the full code

import argparse
import os
from pathlib import Path

from google import genai


MODEL_ID = "gemini-3.8-flash"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Summarize a UTF-8 text file with Gemini."
    )
    parser.add_argument(
        "file",
        type=Path,
        help="Path to the UTF-8 text file to summarize.",
    )
    return parser.parse_args()


def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def build_prompt(text: str) -> str:
    return f"Summarize the text in one concise paragraph of no more than 100 words.\n\nText to summarize:\n{text}"


def main() -> None:
    args = parse_args()
    text = read_text(args.file)

    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text),
        )
        print(interaction.output_text)
    finally:
        client.close()


if __name__ == "__main__":
    main()

How to Use This Reference

Compare the imports, functions, blank lines, and indentation with your saved file. Each name must match for the CLI flow to work.

Before you run TextBrief, what do you expect the terminal to show after it reads the two-paragraph article?

  • Return to the Terminal session where .venv is active.
  • Run TextBrief against the sample article with this command:
python3 summarize.py sample.txt

What Should You See?

You should see one concise paragraph describing how neighborhood libraries are expanding their services in response to community needs. The exact wording can vary between requests.

That is your first working TextBrief result. Your CLI now turns a local article into a relevant Gemini summary.

Summary Did Not Appear?

  • Confirm that sample.txt sits in the same textbrief folder as summarize.py.
  • Confirm that the Terminal prompt still shows the active .venv environment.
  • Confirm that you are using the same Terminal session where the Gemini API key was exported.

Still blocked? Help me troubleshoot why TextBrief does not print a summary for sample.txt.

TextBrief can now summarize a real file from one terminal command. Next, you will add selectable output styles before testing how the current CLI handles a missing file.

Add Summary Styles

Your first TextBrief run turned sample.txt into a concise Gemini summary. That proves the file-to-summary path works.

The first build offers only one output format. A reader may want scannable points from the same command-line interface.

Its file path has only been tested with a valid file. After adding styles, you will test a path that is absent.

In this step, get ready to:
  • Define instructions for brief summaries and bulleted summaries.
  • Add a --style option that controls the generated format.
  • Test how the CLI responds to a file path that does not exist.
Add style instructions

The STYLE_INSTRUCTIONS dictionary connects each style name to a prompt instruction. This keeps output rules in one place.

  • In summarize.py, find the MODEL_ID assignment.
  • Place the style mapping directly below MODEL_ID by pasting this code:
STYLE_INSTRUCTIONS = {
    "brief": "Summarize the text in one concise paragraph of no more than 100 words.",
    "bullets": "Summarize the text as 3 to 5 concise bullet points.",
}

What does this mapping do?

  • The brief entry preserves the concise paragraph format from your first build.
  • The bullets entry asks for 3 to 5 scannable points.
  • The style names become the choices your command-line option accepts.
  • Save summarize.py.
  • Confirm the original brief behavior by running:
python3 summarize.py sample.txt

What should you see?

You should still see a concise paragraph about neighborhood libraries. This checkpoint proves the new mapping is valid Python.

Seeing a syntax error?

  • Check that both dictionary entries end with commas.
  • Check that the closing brace aligns with STYLE_INSTRUCTIONS.

Still stuck? Help me check my STYLE_INSTRUCTIONS dictionary for a Python syntax error..

Add the --style option

The argparse parser can restrict an option to approved values. A default keeps the original brief behavior when the option is omitted.

  • In parse_args(), find the positional file argument.
  • Insert the style argument after that block by pasting this code:
    parser.add_argument(
        "--style",
        choices=("brief", "bullets"),
        default="brief",
        help="Summary format. Default: brief.",
    )

How does the option work?

  • The choices setting accepts only brief or bullets.
  • The default setting selects brief when the command does not include a style.
  • The selected value becomes available through args.style.
  • Save summarize.py.
  • Verify that the default still produces a brief summary by running:
python3 summarize.py sample.txt

What does the default prove?

You should see another concise paragraph about the library article. The existing command still works without an explicit style.

Does the command reject your arguments?

  • Check that the new parser.add_argument() block appears before return parser.parse_args().
  • Check that --style starts with two hyphens.

Need another pair of eyes? Help me troubleshoot my argparse style option..

The parser now records the chosen format in args.style. The prompt builder needs that value before the API request can use it.

  • In summarize.py, select everything from def build_prompt(text: str) -> str: through the final client.close() line in main().
  • Replace the selected code by pasting this version:
def build_prompt(text: str, style: str) -> str:
    return f"{STYLE_INSTRUCTIONS[style]}\n\nText to summarize:\n{text}"


def main() -> None:
    args = parse_args()
    text = read_text(args.file)

    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text, args.style),
        )
        print(interaction.output_text)
    finally:
        client.close()

How does the style reach Gemini?

  • The style parameter tells build_prompt() which dictionary entry to use.
  • The dictionary lookup places the matching instruction above the source text.
  • The API call passes args.style into the prompt builder.
  • Save summarize.py.

Before you run this, how do you expect the same article to look when bullets controls the prompt?

  • Generate the bulleted summary by running:
python3 summarize.py sample.txt --style bullets

What should you see?

You should see 3 to 5 concise bullet points about the library's services and community role. The wording can vary because AI summaries are generated each time.

That is the new style control working. One source file can now produce two useful formats.

Still seeing one paragraph?

  • Check that build_prompt() accepts both text and style.
  • Check that the API call uses build_prompt(text, args.style).
  • Check that the dictionary key is spelled bullets.

Still getting the brief format? Help me trace how --style bullets moves from argparse into my Gemini prompt..

✔️ Awesome, I've got everything!

Your saved summarize.py now supports brief and bulleted summaries.

ⓧ I'd like to double check the full code

import argparse
import os
from pathlib import Path

from google import genai


MODEL_ID = "gemini-3.8-flash"
STYLE_INSTRUCTIONS = {
    "brief": "Summarize the text in one concise paragraph of no more than 100 words.",
    "bullets": "Summarize the text as 3 to 5 concise bullet points.",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Summarize a UTF-8 text file with Gemini."
    )
    parser.add_argument(
        "file",
        type=Path,
        help="Path to the UTF-8 text file to summarize.",
    )
    parser.add_argument(
        "--style",
        choices=("brief", "bullets"),
        default="brief",
        help="Summary format. Default: brief.",
    )
    return parser.parse_args()


def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def build_prompt(text: str, style: str) -> str:
    return f"{STYLE_INSTRUCTIONS[style]}\n\nText to summarize:\n{text}"


def main() -> None:
    args = parse_args()
    text = read_text(args.file)

    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text, args.style),
        )
        print(interaction.output_text)
    finally:
        client.close()


if __name__ == "__main__":
    main()

How the styles connect

This reference combines the style dictionary with the parser option and prompt lookup. The selected format reaches Gemini through args.style.

Test a missing path

The CLI now handles output style. A dependable tool also needs predictable behavior when its input path is absent.

Before you run this, how do you think TextBrief will respond to a path it cannot read?

  • Test the absent path by running:
python3 summarize.py missing.txt

The raw traceback is the result

You should see a Python traceback that ends with FileNotFoundError. The terminal exposes internal failure details instead of one clear message.

This failure is intentional. read_text() sends the path straight to the UTF-8 read without checking whether the file exists.

TextBrief now produces brief or bulleted summaries from the same file. You also exposed the raw missing-file failure that the next step replaces with a clear one-line error.

Handle Friendly CLI Errors

Your selectable summary styles now work. The missing-path test also exposed a raw Python traceback that buries the useful message.

This step adds input validation plus exception handling to your CLI. A missing path becomes one clear error while valid summaries keep working.

In this step, get ready to:
  • Validate paths before reading their contents.
  • Separate local input failures from Gemini API failures.
  • Return friendly errors without exposing Python tracebacks.
Validate files before reading

A file path can point to a missing item. It can also point to a folder or a file that is unsuitable for UTF-8 text processing.

  • In summarize.py, find the current read_text() function:
def read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")

What are you replacing?

The current function immediately reads whatever path it receives. Any filesystem or decoding failure escapes as a traceback.

  • Replace the current read_text() function with this validated version:
def read_text(path: Path) -> str:
    if not path.exists():
        raise FileNotFoundError(f"{path} does not exist.")
    if not path.is_file():
        raise ValueError(f"{path} is not a regular file.")

    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError as error:
        raise ValueError(f"{path} is not a UTF-8 text file.") from error

    if not text.strip():
        raise ValueError(f"{path} is empty.")

    return text

What does this function protect?

  • The existence check gives a missing path its own clear message.
  • The regular-file check rejects folders before a read begins.
  • The decoding guard converts invalid UTF-8 content into a useful input error.
  • The final check rejects files containing no meaningful text.
  • Save summarize.py.

Before you run this check, which part of the traceback do you think changes first?

  • Test the missing path by running:
python3 summarize.py missing.txt

The targeted message is in place

The traceback still appears because main() does not handle the exception yet. Its final line now says that missing.txt does not exist.

This intermediate failure is expected. The next changes turn that message into the complete user-facing output.

Still seeing the original filesystem message?

  • Confirm that you replaced the original two-line read_text() function.
  • Check that the existence test appears before the file read.

Need help finding the mismatch? Help me compare my read_text function with the validated version.

Isolate Gemini failures

Input validation covers local files. Gemini requests need their own boundary so credential problems and API failures can be handled separately.

  • In summarize.py, find the import block at the top of the file:
import argparse
import os
from pathlib import Path

from google import genai

Why update the imports?

The current imports can read files and call Gemini. They cannot yet route errors to the terminal error stream or identify SDK API failures.

  • Replace the import block with this expanded version:
import argparse
import os
import sys
from pathlib import Path

from google import genai
from google.genai import errors

What do the new imports provide?

  • The sys module gives the CLI access to its error stream.
  • The errors import exposes the SDK exception used for Gemini API failures.
  • Below build_prompt(), add this summary helper:
def summarize_text(text: str, style: str) -> str:
    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text, style),
        )
        summary = interaction.output_text
    finally:
        client.close()

    if not summary:
        raise RuntimeError("Gemini returned no text summary.")

    return summary.strip()

What does this helper control?

  • The credential check stops before the request when the environment variable is unavailable.
  • The client closes even when the request raises an exception.
  • The output check prevents an empty Gemini response from looking like a successful summary.
  • The final cleanup removes extra whitespace from the returned text.
  • Save summarize.py.
  • Confirm the script remains runnable by running:
python3 summarize.py sample.txt --style bullets

What does this check prove?

You should still see 3 to 5 bullet points about neighborhood libraries. This confirms that the new imports and helper leave the file runnable.

Did the bullet summary stop working?

  • Check that summarize_text() sits below the complete build_prompt() function.
  • Confirm that the virtual environment remains active in the current Terminal session.

Still stuck? Help me debug the new summarize_text function.

Return friendly errors from main

The main() function becomes the boundary between internal exceptions and terminal output. It can now send failures to the error stream while returning a nonzero status.

  • At the bottom of summarize.py, find the existing main() function plus its execution guard:
def main() -> None:
    args = parse_args()
    text = read_text(args.file)

    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text, args.style),
        )
        print(interaction.output_text)
    finally:
        client.close()


if __name__ == "__main__":
    main()

What changes in this block?

The current function performs file reading and Gemini communication directly. Exceptions escape because no handler sits around those operations.

  • Replace the existing bottom block with this error-handling version:
def main() -> int:
    args = parse_args()

    try:
        text = read_text(args.file)
        summary = summarize_text(text, args.style)
    except errors.APIError as error:
        print(
            f"Gemini API error ({error.code}): {error.message}",
            file=sys.stderr,
        )
        return 1
    except (OSError, ValueError, RuntimeError) as error:
        print(f"Error: {error}", file=sys.stderr)
        return 1

    print(summary)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

How does the CLI respond now?

  • The first handler preserves the code and message supplied by a Gemini API failure.
  • The second handler converts local input and credential failures into one-line errors.
  • A failure returns status 1 while a successful summary returns status 0.
  • The execution guard passes that status back to the shell.
  • Save summarize.py.

Before the final checks, which command should fail cleanly? Which command should still return bullet points?

  • Verify the friendly missing-file path by running:
python3 summarize.py missing.txt

What should you see?

The terminal should print Error: missing.txt does not exist. on one line. You should see no traceback.

  • Verify that successful summaries still work by running:
python3 summarize.py sample.txt --style bullets

What confirms success?

You should see 3 to 5 concise bullet points about the library article. The error boundary protects failures without changing successful output.

Still seeing a traceback?

  • Confirm that both exception handlers are indented under the same try block in main().
  • Check that the execution guard calls raise SystemExit(main()).
  • Confirm that sys and errors appear in the import block.

Need another pair of eyes? Help me find why TextBrief still prints a traceback.

✔️ Awesome, I've got everything!

Great. Save summarize.py once more before you capture the result.

ⓧ I'd like to double check the full code

import argparse
import os
import sys
from pathlib import Path

from google import genai
from google.genai import errors


MODEL_ID = "gemini-3.8-flash"
STYLE_INSTRUCTIONS = {
    "brief": "Summarize the text in one concise paragraph of no more than 100 words.",
    "bullets": "Summarize the text as 3 to 5 concise bullet points.",
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Summarize a UTF-8 text file with Gemini."
    )
    parser.add_argument(
        "file",
        type=Path,
        help="Path to the UTF-8 text file to summarize.",
    )
    parser.add_argument(
        "--style",
        choices=("brief", "bullets"),
        default="brief",
        help="Summary format. Default: brief.",
    )
    return parser.parse_args()


def read_text(path: Path) -> str:
    if not path.exists():
        raise FileNotFoundError(f"{path} does not exist.")
    if not path.is_file():
        raise ValueError(f"{path} is not a regular file.")

    try:
        text = path.read_text(encoding="utf-8")
    except UnicodeDecodeError as error:
        raise ValueError(f"{path} is not a UTF-8 text file.") from error

    if not text.strip():
        raise ValueError(f"{path} is empty.")

    return text


def build_prompt(text: str, style: str) -> str:
    return f"{STYLE_INSTRUCTIONS[style]}\n\nText to summarize:\n{text}"


def summarize_text(text: str, style: str) -> str:
    if not os.getenv("GEMINI_API_KEY"):
        raise RuntimeError("GEMINI_API_KEY is not set.")

    client = genai.Client()
    try:
        interaction = client.interactions.create(
            model=MODEL_ID,
            input=build_prompt(text, style),
        )
        summary = interaction.output_text
    finally:
        client.close()

    if not summary:
        raise RuntimeError("Gemini returned no text summary.")

    return summary.strip()


def main() -> int:
    args = parse_args()

    try:
        text = read_text(args.file)
        summary = summarize_text(text, args.style)
    except errors.APIError as error:
        print(
            f"Gemini API error ({error.code}): {error.message}",
            file=sys.stderr,
        )
        return 1
    except (OSError, ValueError, RuntimeError) as error:
        print(f"Error: {error}", file=sys.stderr)
        return 1

    print(summary)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

How to use this reference

Compare each function with your saved file. Pay special attention to indentation inside the two exception handlers.

That rough edge is gone: TextBrief now reports input failures clearly while successful brief and bullet summaries keep working.

Secret mission

Save Summaries to a File

Terminal summaries can disappear into scrollback as soon as your session gets busy. Extend TextBrief with an optional output path that saves each generated summary as a reusable UTF-8 text file.

Clean Up Your Resources

Clean Up Your Resources

The TextBrief project used the Gemini Developer API free tier without paid billing. Decide whether to keep your resources available, pause your local session, or delete everything.

Resources you used:

  • The local textbrief folder containing .venv, requirements.txt, smoke_test.py, sample.txt, summarize.py, and summary.txt.
  • The Gemini API key stored in Google AI Studio.

Keep everything running

No action is needed. Choose this option if you plan to keep improving TextBrief or summarizing more files.

  • Keep the textbrief folder in its current location.
  • Keep the Gemini API key active in Google AI Studio.
  • Use only non-sensitive text files because free-tier content may be used to improve Google products.

Pause - I'll come back to this later

Pausing ends the active local session while preserving your project files. Your Google AI Studio key remains available when you return.

  • Deactivate .venv by running this command:
deactivate

What Deactivation Changes

This command stops using the virtual environment in the current Terminal session. The .venv folder stays inside textbrief.

  • Close Terminal to end the current zsh session.

Closing Terminal clears the session-only GEMINI_API_KEY export. Your local files remain ready for your next session.

Delete - I don't want to use this again

Remove the API key plus the complete local project if you no longer plan to use TextBrief. This leaves no active project credential or local files behind.

Deletion Is Permanent

Deleting these resources can feel drastic. The steps below remove only the TextBrief API key plus the local textbrief folder.

The local folder includes your saved summary.txt output. Keep a separate copy if you still need that summary.

  • Return to the Google AI Studio API Keys page in your browser.
  • Select the API key you created for TextBrief.
  • Delete or revoke the selected API key.
  • Confirm that the API Keys page no longer shows the key as active.

The key can no longer authorize new Gemini requests. Your credential cleanup is complete.

  • Deactivate .venv by running this command:
deactivate

Why Deactivate First

Deactivation ends the virtual environment session before its files are removed. The current Terminal remains inside the textbrief folder.

  • Move out of textbrief plus permanently delete the folder by running this command:
cd .. && rm -rf textbrief

What This Command Removes

The command moves Terminal into the folder that contains textbrief. It then removes the complete project folder without moving it to Trash.

This removes the source files, sample input, saved summary, virtual environment, plus the installed project dependency.

  • Close Terminal to clear the current GEMINI_API_KEY export.

Cleanup complete. TextBrief no longer has an active API key or local project folder.

Nice Work!

Nice Work!

You did it! TextBrief is now a working Python CLI that turns local UTF-8 files into AI summaries through the Gemini Developer API.

You've learned how to:

  • Build a local file summarization workflow with pathlib.Path that turns a filesystem path into a concise Gemini response.
  • Use argparse to offer brief and bulleted summary styles from the terminal.
  • Protect API credentials with the GEMINI_API_KEY environment variable. Add friendly failure handling for invalid files, missing credentials, API errors, and nonzero exit statuses.
  • Secret Mission: Extended TextBrief with an optional output path that saves generated summaries as reusable UTF-8 text files.

Ready to quiz yourself?