Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Virtual Environments and Unit Testing with Pytest

Recommended resources

Hello! Welcome to the final lesson in our Python Foundations module. In our last session, we mastered several key modules from Python's standard library: os, sys, datetime, and collections. You learned how to write scripts that can interact with the file system, handle command-line arguments, manage dates and times, and use specialized data structures.

Today, we shift from writing code to managing it professionally. This lesson addresses two indispensable practices for any serious software developer: managing dependencies with virtual environments and writing unit tests using pytest.

Why are these topics so important for your goal of becoming a Full Stack AI Developer?

  • Reproducibility: When you build an AI model, you need to ensure that you, or anyone else, can recreate the exact software environment it was trained in. Virtual environments make this possible.
  • Reliability: Your AI applications will be built from many small components. Unit tests verify that each component works correctly, giving you the confidence to build, refactor, and expand complex systems without breaking them.

By the end of this lesson, you will be able to set up isolated project environments and write automated tests to ensure your code is robust and maintainable.

1. Dependency Management with Virtual Environments

Imagine you are working on two different projects. Project A requires version 1.0 of a library, but Project B needs version 2.0, which has breaking changes. If you install these libraries globally on your computer, one of your projects will inevitably fail. This problem is often called "dependency hell."

A virtual environment solves this by creating an isolated directory for each project, containing its own Python interpreter and its own set of installed libraries.

Back to basics with pip and venv

To understand the 'why' behind virtual environments, let's start with this excellent article from Bite code!.

Please read the section titled 'What is a virtual environment, and why to use one?'. It uses a clear example to illustrate the problem of dependency conflicts.

Conceptually, a virtual environment is just a folder that bundles together the necessary components for your project to run in isolation.

Structure of a Python Virtual Environment (VENV)
This diagram shows the core components of a virtual environment: a copy of the Python interpreter, the `pip` package manager, activation scripts for your terminal, and a local `site-packages` directory where project-specific libraries are installed. This entire structure is self-contained within your project's folder.

Using venv: The Standard Approach

Python comes with a built-in module called venv for creating virtual environments. It's the standard, "batteries-included" way to manage project dependencies.

The following video from Tech With Tim provides a clear, step-by-step tutorial on the entire venv workflow. We will walk through the key parts together.

Python Virtual Environments - Full Tutorial for Beginners

This tutorial will guide you through creating, activating, and using a virtual environment with Python's built-in venv module.

Watch from the beginning to 04:35. The video covers: What a virtual environment is (00:00 - 00:36) Creating an environment using python -m venv <env_name> (00:36 - 02:12) Activating and deactivating the environment (02:12 - 03:12) Installing packages with pip inside the environment (03:12 - 04:35)

Key Commands Summary:

  1. Create: Navigate to your project folder in the terminal and run:
    
    
    
    # It's conventional to name the environment '.venv' or 'venv'
    python -m venv .venv 
    
  2. Activate:
    • On macOS/Linux: source .venv/bin/activate
    • On Windows: .venv\Scripts\activate
      You'll know it's active because the environment's name will appear in your terminal prompt (e.g., (.venv) Your-Computer:~$).
  3. Install: Use pip as usual. The packages will be installed only inside the active environment.
    pip install requests
    
  4. Deactivate: Simply run deactivate.

Sharing and Recreating Environments with requirements.txt

Virtual environment folders are specific to your machine and should not be committed to version control (e.g., Git). So how do you share your project's dependencies with others or deploy it to a server?

The standard method is to create a requirements.txt file. This file is a simple list of the packages and their exact versions needed for the project.

Python Virtual Environments - Full Tutorial for Beginners

Now, let's watch the second part of the same video to learn how to manage and share your project's dependencies.

Watch from 04:35 to 07:07. Focus on these two commands: pip freeze > requirements.txt: Creates the requirements file. pip install -r requirements.txt: Installs all packages listed in the file.

This workflow is fundamental to collaborative and professional Python development. When someone new joins your project, they can create their own virtual environment, activate it, and run pip install -r requirements.txt to get a perfect copy of the necessary dependencies.

Python Virtual Environment (venv) and Requirements
This image provides a visual recap of the workflow on a Windows command prompt: creating the `venv`, activating it, and then using `pip freeze` to see the installed packages, which can then be saved to a `requirements.txt` file.

A Glimpse at Modern Tooling: pipenv

While venv and pip are the fundamental tools, the Python ecosystem has developed higher-level tools that streamline this process. pipenv is one such tool that aims to combine package management and virtual environment management into a single command-line utility.

It introduces two new files:

  • Pipfile: A human-readable file where you declare the packages your project needs (e.g., requests).
  • Pipfile.lock: An auto-generated file that locks the exact versions of all your packages and their dependencies. This ensures "deterministic builds"—meaning everyone who installs from the lock file gets the exact same environment, every single time.

You don't need to master pipenv now, but being aware of it is important for your career.

Python Tutorial: Pipenv - Easily Manage Packages and Virtual Environments

Let's get a brief conceptual overview of pipenv from this video by Corey Schafer. Focus on what problems it solves rather than the specific commands.

Please watch these two segments: Introduction (00:00 - 02:15): Understand the motivation behind combining pip and virtualenv. Pipfile and Pipfile.lock Explained (05:04 - 11:04): Grasp the difference between these two files and why the .lock file is so important for reproducibility.

For now, stick with venv and requirements.txt as they are the universal standard.


2. Writing Unit Tests with pytest

Now that your project environment is isolated and reproducible, let's ensure the code within it is correct and reliable. Unit testing is the practice of writing small, automated tests for individual "units" of code—typically functions or methods.

How to Use Pytest for Unit Testing

This article from DataCamp provides a great introduction to the concept of unit testing and its importance.

Read the first two sections: 'What are unit tests?' and 'Why do we need unit tests?'. Pay attention to the benefits: preserving resources, providing documentation, and boosting developer confidence.

While Python has a built-in unittest module, the community has largely adopted pytest as the de-facto standard due to its simplicity and powerful features.

Why pytest?

pytest lets you write tests as simple Python functions using the standard assert statement. This results in tests that are more readable and require less boilerplate code compared to unittest.

How to Use Pytest for Unit Testing

Let's explore why pytest is so popular. The same DataCamp article has a great comparison.

Read the sections 'Why use Pytest?' and 'Pytest vs unittest'. Note the key differences in code structure (boilerplate) and the detailed output pytest provides on failure.

Writing Your First Test

Let's walk through the process of writing and running a simple test.

  1. Install pytest: Make sure your virtual environment is activated, then run:

    pip install pytest
    
  2. Create a function to test: Create a file named my_math.py with a simple function.

    
    
    
    # my_math.py
    def add(x, y):
        """Adds two numbers together."""
        return x + y
    
  3. Write the test: pytest automatically discovers test files named test_*.py or *_test.py and functions within them named test_*. Create a file named test_my_math.py.

    
    
    
    # test_my_math.py
    from my_math import add
    
    def test_add_positive_numbers():
        """Tests addition of two positive integers."""
        assert add(2, 3) == 5
    
    def test_add_negative_numbers():
        """Tests addition of two negative integers."""
        assert add(-1, -1) == -2
        
    def test_add_mixed_numbers():
        """Tests addition of a positive and negative integer."""
        assert add(5, -3) == 2
    

    Notice how clean this is. Each test is a simple function with a clear assert statement.

  4. Run the tests: In your terminal, from the project's root directory, simply run the pytest command.

    pytest
    

    pytest will discover and run your tests, reporting the results. If an assert fails, it will provide a detailed report showing exactly what the expected and actual values were.

Parametrizing Tests

What if you want to test the add function with many different inputs? You could write a dozen test_* functions, but that's repetitive. pytest has a powerful feature called parametrization that lets you run the same test function with multiple sets of inputs.

How to Use Pytest for Unit Testing

The parametrize decorator is one of pytest's most powerful features for reducing boilerplate code. Let's see how it works in the DataCamp article.

Read the section 'Pytest parametrize'. Understand how the @pytest.mark.parametrize decorator is used to define input arguments and their expected outputs in a compact way.

Using this feature, we can rewrite our tests for the add function more concisely:




# test_my_math.py (revised)
import pytest
from my_math import add

@pytest.mark.parametrize("x, y, expected", [
    (2, 3, 5),          # Test case 1: positive numbers
    (-1, -1, -2),       # Test case 2: negative numbers
    (5, -3, 2),         # Test case 3: mixed numbers
    (0, 0, 0),          # Test case 4: zeros
    (1.5, 2.5, 4.0)     # Test case 5: floats
])
def test_add(x, y, expected):
    """Tests the add function with various inputs."""
    assert add(x, y) == expected

Now, one test function covers five different scenarios! pytest will run this test five separate times, one for each tuple in the list, and report any failures individually.


3. Practice Exercise

Let's combine everything you've learned. Your task is to set up a new project with a virtual environment and write tests for a utility module.

  1. Create a new directory for your project (e.g., python-utils).
  2. Navigate into that directory and create a virtual environment named .venv.
  3. Activate the environment.
  4. Install pytest using pip.
  5. Create a file named validators.py with the following function:
    
    
    
    # validators.py
    import re
    
    def is_valid_email(email: str) -> bool:
        """
        Checks if a string is a valid email format.
        This is a simple regex and not exhaustive.
        """
        if not isinstance(email, str):
            return False
    
    
    
        # A simple regex for email validation
        pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
        return re.match(pattern, email) is not None
    
  6. Create a test file test_validators.py.
  7. Inside test_validators.py, write a parametrized test function test_is_valid_email that checks multiple cases:
    • A few valid emails (e.g., test@example.com, user.name@domain.co.uk).
    • A few invalid emails (e.g., test@.com, user@domain, plainaddress).
    • A non-string input (e.g., 12345).
  8. Run pytest from your terminal and ensure all tests pass.
  9. Finally, create a requirements.txt file for your project.
Click here for a solution to review

File test_validators.py:

import pytest
from validators import is_valid_email

@pytest.mark.parametrize("email, expected", [



    # Valid cases
    ("test@example.com", True),
    ("user.name+tag@gmail.co.uk", True),
    ("user_name@sub.domain.org", True),
    



    # Invalid cases
    ("plainaddress", False),
    ("test@.com", False),
    ("@missingusername.com", False),
    ("username@.com", False),
    ("username@domain", False),
    ("username@domain.", False),
    ("username@domain.c", False),




    # Edge cases / wrong types
    ("", False),
    (None, False),
    (12345, False),
    ("test@domain..com", False) # Double dot
])
def test_is_valid_email(email, expected):
    """Tests the is_valid_email function with various inputs."""
    assert is_valid_email(email) == expected

Commands to run:




# In your project directory
pytest
pip freeze > requirements.txt

Your requirements.txt should contain pytest and its dependencies.


Conclusion

This lesson, and this module, have laid the groundwork for your journey as a Python developer. You've gone from basic syntax to object-oriented programming, file handling, and now, professional project management practices.

Key Takeaways:

  • Virtual Environments (venv) are essential for isolating project dependencies and ensuring reproducibility.
  • pip freeze > requirements.txt is the standard way to record your project's dependencies for sharing and deployment.
  • Unit Tests are automated checks that verify your code works as expected, catching bugs early.
  • pytest is the modern standard for testing in Python, offering a simple syntax (assert) and powerful features like parametrization (@pytest.mark.parametrize).

This completes our Python Programming Foundations module. You now have a solid command of the language. In the next module, Databases and SQL Fundamentals, we will begin to build the backend of a real application by learning how to store, structure, and query data, starting with an introduction to database normalization.