Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Virtual Environments in Python

Build a small Python CLI named Reading Digest that is designed to live in its own project-specific environment, fetches JSON book data with `requests`, and formats a filtered digest for the terminal. Along the way, you will pin dependencies, normalize settings, transform dictionary-based data, and finish a tested command-line workflow.

Lab platform
Lab Info
Level
Beginner
Last updated
Jun 12, 2026
Duration
30m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Get oriented with the Reading Digest CLI project

    This lab walks through a small but realistic Python command-line project. You will build a tool called Reading Digest that retrieves a catalog of books, filters the results based on user-supplied criteria, and displays a formatted reading digest in the terminal.

    You are not just filling in random functions; you are assembling the pieces of a maintainable application that reads configuration, fetches structured data, transforms it, and presents the results cleanly in the terminal.

    A Python project often feels simple at first because a script can run with only a few lines of code. The challenge appears later, when the code depends on external packages, different machines need to reproduce the same setup, or the program grows beyond one file. That is why this lab starts with dependency metadata and repository hygiene before moving into API access and business logic. In production systems, reproducibility matters just as much as correctness. A project that works only on one machine or only with one accidental package version becomes difficult to support.

    The starter code already gives you the project's structure:

    • The app package contains the modules you will complete:
      • Configuration
      • API access
      • Data transformation
      • CLI orchestration
    • The data folder contains sample JSON used for local development.
    • dev_server.py can serve the sample data over HTTP so you can exercise the application without relying on a third-party service.
    • The tests folder is organized by step, making it easy to verify your progress as you complete each task.

    Key terminology:

    • requirements.txt: A plain-text manifest that tells pip which packages and versions to install.
    • virtual environment: A project-local Python environment that isolates packages from other projects.
    • JSON payload: Structured text data that maps naturally to Python dictionaries and lists.
    • CLI entry point: The function or module that receives user input and coordinates the program.

    What you'll accomplish:

    • Prepare the project for clean dependency management and local isolation.
    • Implement the code that fetches and normalizes JSON catalog data.
    • Add filtering, summarization, and terminal formatting behavior.
    • Finish a complete command-line workflow backed by automated tests. info> This lab experience was developed by the Pluralsight team using an internally developed AI tool. All sections were verified by human experts for accuracy prior to publications. However, content may still contain errors or inaccuracies, and we recommend independent verification.

      To report a problem or provide feedback, click here. Feedback may be used to improve accuracy in accordance with our Privacy Policy.
  2. Challenge

    Step 2: Prepare the isolated project environment

    This step establishes the foundation the rest of the project depends on. Before it makes sense to fetch remote data or format output, the project needs a predictable dependency manifest, a repository that stays clean of local artifacts, and a configuration layer that can adapt to different environments.

    Dependency management is not only about getting a package installed once. It is about making sure the same codebase can be recreated consistently later by you, a teammate, or an automated build system.

    Pinning versions in requirements.txt makes the environment reproducible. Ignoring virtual environment folders and generated caches keeps version control focused on source files instead of machine-specific output. In real teams, these two practices reduce a surprising amount of friction because they eliminate hidden differences between developer machines.

    Configuration deserves its own layer for a similar reason. Hard-coded values are fine for a quick script, but they become awkward when you need to point the same program at different endpoints or tune request behavior. A settings helper creates a clean boundary between environment-specific values and application logic. By normalizing settings once, every downstream function can rely on a stable dictionary shape.

    Within this project, the files for this step live at the top level and in app/config.py. They sit at the edge of the architecture: one part shapes the install experience, another part shapes repository behavior, and the config module shapes how runtime values enter the app.

    Key terminology:

    • pinned version: An exact package version declared with ==.
    • repository artifact: A generated or machine-local file that should not be committed.
    • environment variable: A runtime value supplied from outside the Python source code.
    • normalized setting: A value that has been cleaned and converted into a dependable format.

    What you'll accomplish:

    • Lock the project to exact dependency versions.
    • Ignore virtual environment and cache directories.
    • Build a reusable settings dictionary for later API calls.
    Verification Guidance After completing each task in this step:
    • Review your changes and confirm they match the implementation requirements.
    • Run the automated check for the current task.
    • Resolve any failures before moving to the next task.

    Context

    The project depends on external Python packages. Before building application features, you need to ensure that everyone who installs the project receives the same dependency versions. This first hands-on tasks updates the dependency manifest so the environment remains reproducible.

    Concepts

    Dependency pinning helps ensure that every installation of a project uses the same package versions. In Python projects, this is commonly done through a requirements.txt file. Each line identifies a package, and an exact version pin tells pip to install that specific release rather than the newest compatible version. With the dependency manifest in place, the next improvement addresses source control hygiene. Isolated environments are useful only if their generated files stay local instead of polluting the repository. The next task makes that intent explicit.

    Context

    Development environments often generate files that should not be stored in source control. Local environment folders, bytecode caches, and test cache directories are useful on your machine, but they create noise and merge conflicts when committed accidentally. This task updates the repository ignore rules so local artifacts stay out of Git.

    Concepts

    A .gitignore file tells Git which files and directories to exclude from version control. In Python projects, common examples include virtual environment folders such as venv/ or .venv/, bytecode caches like __pycache__/, and tool-specific caches such as .pytest_cache/. Ignoring these paths does not prevent them from existing; it simply prevents them from being tracked as source. Now that the project is easier to install and safer to share, it needs a predictable way to read runtime values. The next task introduces a configuration layer so later modules can stay focused on their own responsibilities.

    Context

    The API client and CLI should not need to know where configuration values come from. Instead, the application uses a dedicated settings helper to gather configuration in one place and return it in a consistent format. This approach makes the application easier to maintain because configuration changes can be handled in a single location. That design is common in production applications because it keeps configuration concerns separate from business logic.

    Concepts

    Environment variables are string-based runtime inputs, which means they often require conversion and cleanup before the program can use them reliably. A helper such as get_settings acts as a normalization boundary: it trims strings, applies defaults, removes formatting inconsistencies such as trailing slashes, and converts numeric values into integers.

    Once normalized, the settings dictionary becomes a stable contract between modules. This keeps downstream code simpler because it can assume settings already have the right shape and types. You now have the project-level groundwork in place: deterministic dependencies, clean repository rules, and a reliable settings helper. In the next step, you will use that configuration to fetch JSON data and transform raw payloads into useful Python dictionaries.

  3. Challenge

    Step 3: Fetch and normalize JSON data

    This step turns the project from a static scaffold into a data-driven application. You will implement the client-side workflow for requesting a JSON document, validating its structure, and converting raw records into the format the rest of the application expects.

    It is helpful to think of data retrieval as two related but separate problems. The first is transport: making an HTTP request, handling response status, and parsing the response into Python objects. The second is normalization: validating the payload structure and extracting only the fields the application needs. Separating these concerns keeps functions smaller and makes failures easier to diagnose.

    This separation also improves testability. Request logic can be tested independently from payload validation and normalization, allowing each piece of the workflow to be verified in isolation. The tasks in this step follow that same progression: transport, structural validation, record normalization, and a composed retrieval workflow.

    These tasks take place in app/api_client.py, which sits between external data and internal logic. It receives settings from app/config.py and returns clean dictionaries to the transformation layer in app/transformers.py.

    Key terminology:

    • raise_for_status(): A requests method that raises an exception for HTTP error reponses.
    • injected dependency: A function or object supplied from outside so behavior can be tested or swapped.
    • payload normalization: The process of validating and reshaping raw data.
    • endpoint: A URL where a resource can be requested.

    What you'll accomplish:

    • Implement a request helper that returns parsed JSON.
    • Validate the API payload structure before reading individual records.
    • Extract and normalize book records into consistent dictionaries.
    • Compose a higher-level function that fetches and limits catalog results.

    Context

    Before the application can work with catalog data, it needs a helper that retrieves and parses JSON from an API response. This task focuses on the transport (request) layer of the workflow.

    Concepts

    The requests library returns a response object rather than raw JSON directly. That response contains status information, headers, and methods for decoding the body. A good pattern is to call raise_for_status() immediately so failed HTTP responses become explicit exceptions, then call json() only after the status is known to be acceptable.

    Once transport is handled, the next challenge is data quality. JSON can be syntactically valid while still being unusable for your application. The next task introduces structural validation and record cleanup so later functions work with trustworthy dictionaries. With the structural contract enforced, the next task fills in the loop body to extract and clean each individual record. ### Context

    A valid payload structure does not guarantee that every record inside it is usable. Individual items may have missing fields, wrong types, or ratings that cannot be parsed. Handling these cases inside the loop—by skipping bad records rather than failing the whole call—keeps the function resilient to real-world noise. This task focuses on the per-record extraction logic that turns each raw dictionary into a clean, typed entry.

    Concepts

    Skipping individual malformed records with continue is a common pattern when consuming messy external data. It avoids discarding an entire useful response just because one item is broken. Using try/except for the rating conversion is especially important because the field may arrive as a string, a float, or occasionally be absent entirely. You now have separate pieces for transport and normalization. The last task in this step combines them into a single workflow that the CLI can call without worrying about how the data got there. ### Context

    Higher-level application code should not need to know how to construct endpoint URLs, apply timeouts, or parse returned JSON. Those responsibilities belong in a workflow function that composes the lower-level helpers you have already written. This task creates that workflow and gives the rest of the application a simple contract: pass in settings and receive a list of clean book dictionaries. It is a small example of layered architecture in action.

    Concepts

    Composition is the practice of building larger behavior by connecting smaller functions that each do one job well. Here, fetch_books will assemble settings-driven URL creation, network retrieval, payload normalization, and result limiting. This keeps the CLI layer thin and declarative. In larger systems, this pattern reduces duplication and makes changes safer because each concern remains in a single function rather than being reimplemented throughout the codebase. At this point, the application can obtain clean data from a JSON source. The next step shifts from data access to business logic by filtering, ranking, and summarizing the catalog for user-facing output.

  4. Challenge

    Step 4: Transform catalog data into useful views

    This step turns clean catalog dictionaries into useful views for the person reading the output. The source data may already be valid, but users still need ways to narrow the list, prioritize the strongest items, and understand the broader shape of the results.

    In practical terms, you will build three related views of the same underlying catalog. First, you will filter by genre in a forgiving way that supports a no-filter mode. Next, you will enforce a rating threshold and order the results deterministically so users see the best matches first. Finally, you will compute summary counts by genre using a dictionary accumulator. These are common patterns in Python because lists and dictionaries are flexible enough to support rich data shaping without heavy frameworks.

    Transformation functions are where much of the application's value lives. These functions often take in data, return new data, and avoid side effects such as network requests or printing. That makes them easy to reason about and strong candidates for unit tests. A dedicated transformation layer also keeps the CLI simple: instead of embedding filtering rules directly inside the command-line entry point, the program can call focused functions with clear responsibilities.

    Architecture context:

    All of this work happens in app/transformers.py. That module sits in the middle of the application's flow, receiving clean records from the API layer and handing ready-to-display data to the formatter and CLI.

    Key terminology:

    • pure function: A function that returns a result based only on its inputs and does not mutate external state.
    • accumulator: A variable, often a dictionary, that is updated as you iterate through data.
    • threshold filtering: Keeping only records that meet a minimum numeric rule.
    • deterministic order: A stable sort order that produces the same output each time.

    What you'll accomplish:

    • Filter the catalog by an optional genre value.
    • Select books that meet a minimum rating.
    • Produce a per-genre count summary for formatted output. ### Context Users often want a focused view of data rather than the full catalog every time. Genre filtering is a simple but meaningful example because it lets one underlying dataset support multiple use cases without changing the source. This function becomes the first transformation in the output pipeline, narrowing the list before rating and formatting logic are applied. By keeping it independent, the same function could also be reused by tests, scripts, or future features.

    Concepts

    Filtering is one of the most common list operations in Python. The key design choice here is deciding what counts as an active filter versus a request for everything. Treating all or a blank value as a no-filter mode gives the CLI a user-friendly default and avoids special-case logic elsewhere. Case-insensitive comparison is another practical improvement because user input is rarely standardized. Under the hood, the function simply produces a new list based on a boolean condition, which is a classic transformation pattern. Now that the catalog can be narrowed by category, the next improvement adds prioritization. Users typically care not only about whether an item matches a category, but also whether it clears a quality bar and appears in a sensible order. ### Context A filtered list can still be too broad if it includes low-value results. The rating threshold step solves that by enforcing a minimum standard and sorting the surviving entries so the best recommendations rise to the top. This kind of selection logic is common in dashboards, recommendation tools, and search result ranking. By implementing it as a separate function, you keep the rule easy to change later without rewriting the rest of the workflow.

    Concepts

    Selection and sorting are closely related but distinct operations. Selection answers which records should remain, while sorting answers how they should be ordered. Python makes both convenient: you can filter with a list comprehension and sort with sorted(...). Adding a secondary sort key is important when two ratings are equal because it makes the output deterministic. Stable, predictable ordering is especially valuable in tests and in any interface where users compare repeated runs. The final transformation for this step changes perspective from individual items to aggregated insight. Summary counts are a simple way to show structure and context, and they prepare the data for the digest formatter in the next step. ### Context A recommendation list tells a user what matched, but a summary tells the user what the filtered dataset looks like overall. Even when only a few books remain, knowing how many belong to each genre provides context for the selection. This task adds a lightweight aggregation layer that will make the final CLI output feel more complete and intentional. It also gives you another chance to work directly with dictionaries as counting structures.

    Concepts

    An accumulator dictionary is a standard Python technique for grouping or counting values during iteration. Each item contributes to a key, and the count grows as matching items are encountered. Using dict.get(key, 0) is a beginner-friendly way to implement this pattern without extra imports. Returning a sorted dictionary is not strictly required for counting, but it makes downstream rendering deterministic and easier to read, which is a worthwhile tradeoff for small datasets. You now have the core transformation layer: filtered results, ranked recommendations, and a compact summary. In the final step, you will format that data for the terminal and connect every layer through the CLI entry point.

  5. Challenge

    Step 5: Finish the CLI

    This step turns the project into a complete command-line tool. The scaffold already handles dependency injection and error handling; your job is to fill in the three remaining pieces: the output format strings in the digest formatter, the argument definitions for the CLI parser, and the six-line orchestration body that connects every layer.

    Architecture context: app/transformers.py owns presentation logic; app/main.py is the top-level coordinator that reads user intent and calls each layer in order.

    Key terminology:

    • formatter: Code that turns structured data into user-facing text.
    • namespace: The object returned by argparse containing parsed option values.
    • exit code: A numeric result that indicates success or failure to the shell.
    • orchestration: Coordinating multiple components into a full workflow.

    What you'll accomplish:

    • Add the two format strings that complete the digest output.
    • Register the --genre and --min-rating CLI options.
    • Wire the six-step workflow that connects configuration through formatted output. ### Context The build_digest scaffold already builds the header, handles the empty-book case, and outputs the 'Catalog summary:' heading. The only missing pieces are the per-book line inside the loop and the per-genre count line inside the summary loop.

    Concepts

    F-strings with format specifiers such as :.1f produce consistent decimal output regardless of whether the underlying value is a float or an int. The singular/plural distinction ('book' vs 'books') is a small detail that makes output feel polished rather than programmatically generated. With the formatter complete, the next task exposes the filtering controls to the person running the CLI. ### Context A command-line program becomes far more useful when users can supply options instead of editing source code. Adding --genre and --min-rating is enough to expose the two filters the application already supports. Because argparse handles type conversion, the run function receives a float directly rather than a raw string.

    Concepts

    argparse creates a parser object, registers options with add_argument, and returns a namespace whose attributes match each option name. Defaults define the app's behavior when the user runs it with no arguments, so choosing sensible defaults is part of the interface design. The CLI can now accept input and produce formatted output. The final task connects every layer into a single runnable workflow. ### Context The run function already defaults its dependencies and catches RequestException and ValueError with a user-friendly message. The only remaining work is the six lines inside the try block that call each layer in the correct order.

    Concepts

    Orchestration code should connect components in a deliberate order while doing as little business logic as possible. Each step returns a value consumed by the next: settings feed the fetcher, books feed the filters, filtered books feed the summarizer, and all three feed the formatter. Keeping this pipeline explicit makes the flow easy to follow and easy to test. You have finished building the Reading Digest CLI. In the next step you will start the application and see the output it produces.

  6. Challenge

    Step 6: Run the application

    The project is now complete. This step shows you how to start the local development server and run the CLI against it so you can see the digest output in your terminal.

    The project includes a lightweight HTTP server that serves the sample book catalog from data/sample_books.json. Using it means the CLI exercises the full request-parse-format pipeline without depending on an external service. ### Start the development server

    In the Terminal, run:

    python3 dev_server.py
    

    The server starts on http://127.0.0.1:8000 and serves the catalog at /books. Leave it running while you use the CLI.

    Run the CLI

    Click the + and select New Tab > Terminal to open a second terminal. Then, run the application with its default settings:

    python3 -m app.main
    

    This returns all books above the default minimum rating with no genre filter.

    Filter by genre and rating

    Pass --genre and --min-rating to narrow the results:

    python3 -m app.main --genre fantasy --min-rating 4.0
    

    Try a few combinations to see how the digest changes:

    python3 -m app.main --genre "science fiction" --min-rating 4.5
    python3 -m app.main --min-rating 4.8
    

    Override settings with environment variables

    The CLI reads three environment variables so you can point it at a different server or adjust behavior without editing code:

    | Variable | Purpose | Default | |---|---|---| | API_BASE_URL | Base URL for the book catalog API | http://127.0.0.1:8000 | | API_TIMEOUT | Request timeout in seconds | 5 | | FEATURED_LIMIT | Maximum number of books to return | 10 |

    Example:

    FEATURED_LIMIT=3 python3 -m app.main
    

    Stop the server

    Press Control+C in the Terminal running dev_server.py to stop it.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight