Skip to main content

📦 Lesson 9: Virtual Environments & Package Management

Learn to isolate project dependencies with venv, manage packages with pip, organize your code into modules and packages, and structure projects like a professional Python developer.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain why virtual environments are essential for Python development
  • Create, activate, and deactivate virtual environments with venv
  • Install, upgrade, and remove packages with pip
  • Freeze and reproduce dependencies with requirements.txt
  • Understand Python's import system — modules, packages, and __init__.py
  • Structure a Python project with proper layout conventions
  • Use the if __name__ == "__main__" pattern correctly

Estimated Time: 60 minutes

Prerequisites: Basic Python, file system navigation, command line familiarity

In This Lesson

💥 The Dependency Problem

Imagine you're working on two projects:

  • Project A requires requests==2.28.0 (an older version)
  • Project B requires requests==2.31.0 (the latest version)

If both projects share the same Python installation, you can only have one version of requests installed at a time. Upgrading for Project B breaks Project A. Downgrading for Project A breaks Project B. This is called dependency conflict, and it's one of the most common headaches in software development.

🐍 System Python Only ONE version per package! 📁 Project A requests==2.28.0 flask==2.2.0 📁 Project B requests==2.31.0 django==4.2.0 💥 CONFLICT!

The solution? Give each project its own isolated Python environment with its own set of packages. That's exactly what virtual environments do.

🐍 Project A venv requests==2.28.0 ✅ flask==2.2.0 ✅ 📁 Project A Code Isolated, reproducible ✅ No conflicts! 🐍 Project B venv requests==2.31.0 ✅ django==4.2.0 ✅ 📁 Project B Code Isolated, reproducible ✅ No conflicts!

📖 Key Terms

Virtual environment: An isolated Python installation with its own python binary and pip, separate from the system Python. Packages installed in one venv don't affect others.

Package: A reusable library distributed via PyPI (Python Package Index). Examples: requests, flask, pandas.

Dependency: A package that your project needs to function. Dependencies can have their own dependencies (transitive dependencies).

PyPI (Python Package Index): The official repository at pypi.org hosting over 500,000 Python packages.

🏗️ Creating Virtual Environments

Python 3 includes the venv module in its standard library — no extra installation needed.

Create a Virtual Environment

# Navigate to your project directory
# cd my_project

# Create a virtual environment named "venv" (convention)
# python -m venv venv

# Or name it anything you want
# python -m venv .venv
# python -m venv my_env

This creates a new directory (e.g., venv/) containing:

# venv/
# ├── bin/            (Linux/macOS) or Scripts/ (Windows)
# │   ├── python      → isolated Python binary
# │   ├── pip         → isolated pip
# │   └── activate    → activation script
# ├── lib/
# │   └── pythonX.Y/
# │       └── site-packages/   → where installed packages go
# ├── include/
# └── pyvenv.cfg      → configuration file

Activate the Virtual Environment

Activating a venv modifies your shell's PATH so that python and pip point to the venv's versions:

# Linux / macOS
# source venv/bin/activate

# Windows (Command Prompt)
# venv\Scripts\activate.bat

# Windows (PowerShell)
# venv\Scripts\Activate.ps1

# Windows (Git Bash)
# source venv/Scripts/activate

When activated, your terminal prompt changes to show the venv name:

# Before activation:
# $ python --version
# Python 3.12.0   ← system Python

# After activation:
# (venv) $ python --version
# Python 3.12.0   ← venv Python (same version, isolated packages)

# Verify which python is being used:
# (venv) $ which python     (Linux/macOS)
# /home/user/my_project/venv/bin/python

# (venv) $ where python     (Windows)
# C:\Users\user\my_project\venv\Scripts\python.exe

Deactivate

# Return to system Python
# (venv) $ deactivate
# $   ← back to normal prompt

🧠 What Activation Actually Does

Activation doesn't "enter" the venv directory — it just prepends the venv's bin/ (or Scripts/) directory to your PATH environment variable. This means when you type python or pip, the shell finds the venv's copies first. You can use the venv without activating it by calling the full path: venv/bin/python myscript.py.

⚠️ Never Commit Your venv

The venv/ directory can be hundreds of megabytes and is platform-specific. Never add it to version control. Instead, add it to .gitignore and share a requirements.txt file so others can recreate the environment.

# .gitignore
venv/
.venv/
__pycache__/
*.pyc

📥 Installing Packages with pip

pip is Python's package installer. With your venv activated, any packages you install go into the venv — not the system Python.

Basic Installation

# Install the latest version
# pip install requests

# Install a specific version
# pip install requests==2.31.0

# Install minimum version
# pip install requests>=2.28.0

# Install compatible version (same major.minor)
# pip install requests~=2.31.0   (allows 2.31.x but not 2.32.0)

# Install multiple packages at once
# pip install requests flask pandas

Managing Installed Packages

# List all installed packages
# pip list

# Show details about a specific package
# pip show requests
# Name: requests
# Version: 2.31.0
# Summary: Python HTTP for Humans.
# Requires: certifi, charset-normalizer, idna, urllib3
# Required-by: (nothing — no other installed package depends on it)

# Upgrade a package to latest
# pip install --upgrade requests

# Uninstall a package
# pip uninstall requests

# Check for outdated packages
# pip list --outdated

Installing from Different Sources

# From a requirements file
# pip install -r requirements.txt

# From a Git repository
# pip install git+https://github.com/user/repo.git

# From a local directory (editable/development mode)
# pip install -e .
# pip install -e ./my_package

# From a wheel file
# pip install my_package-1.0.0-py3-none-any.whl
graph LR A["🔍 pip install requests"] --> B["📡 Search PyPI"] B --> C["⬇️ Download Package"] C --> D["📦 Install Dependencies"] D --> E["✅ Available in venv"] style A fill:#6366f1,color:#fff style E fill:#10b981,color:#fff

📖 pip vs. pip3

On some systems, pip points to Python 2 and pip3 points to Python 3. To avoid confusion, always use python -m pip install ... — this guarantees you're using the pip that matches your active Python interpreter.

📋 Managing Dependencies

A requirements.txt file is the standard way to declare and share your project's dependencies. It lists every package your project needs, with exact version numbers, so anyone can recreate the same environment.

Creating requirements.txt

# Freeze current environment to a file
# pip freeze > requirements.txt

# This produces something like:
# certifi==2023.7.22
# charset-normalizer==3.2.0
# idna==3.4
# requests==2.31.0
# urllib3==2.0.4

Recreating an Environment

# On a new machine or after cloning a repo:

# 1. Create a fresh virtual environment
# python -m venv venv

# 2. Activate it
# source venv/bin/activate   (Linux/macOS)
# venv\Scripts\activate      (Windows)

# 3. Install all dependencies from requirements.txt
# pip install -r requirements.txt

# Done! Your environment matches the original exactly.

Writing requirements.txt by Hand

You can also write requirements.txt manually for cleaner control. Use comments and flexible version specifiers:

# requirements.txt — My Awesome Project
# Core dependencies
requests>=2.28.0,<3.0.0
flask~=2.3.0
sqlalchemy>=2.0

# Data processing
pandas>=2.0
numpy>=1.24

# Development only (consider a separate dev-requirements.txt)
# pytest>=7.0
# black>=23.0
# mypy>=1.0

Splitting Dev vs. Production

# requirements.txt — production dependencies
requests>=2.28
flask~=2.3
gunicorn>=21.0

# dev-requirements.txt — development/testing tools
-r requirements.txt    # Include production deps
pytest>=7.0
pytest-cov>=4.0
black>=23.0
mypy>=1.0
flake8>=6.0
# Install production deps only (deployment)
# pip install -r requirements.txt

# Install everything including dev tools (development)
# pip install -r dev-requirements.txt
① Develop pip install ... Add packages as needed ② Freeze pip freeze > ... Lock exact versions ③ Share git commit requirements.txt in repo ④ Reproduce pip install -r ... Identical env anywhere The Dependency Workflow Develop → Freeze → Share → Reproduce

🧠 pip freeze vs. Hand-Written requirements

pip freeze captures every installed package, including transitive dependencies. This guarantees exact reproducibility but can be noisy. A hand-written requirements.txt with just your direct dependencies is cleaner but may produce slightly different environments on different machines. For production, prefer pip freeze for safety. For open-source libraries, prefer flexible version ranges.

🧩 Python Modules

A module is simply a .py file. When you write import mymodule, Python looks for mymodule.py and executes its code, making its functions, classes, and variables available to you.

Creating and Importing a Module

# ── helpers.py ──
"""Utility functions for data processing."""

def clean_text(text):
    """Remove extra whitespace and lowercase."""
    return " ".join(text.lower().split())

def validate_email(email):
    """Basic email validation."""
    return "@" in email and "." in email.split("@")[-1]

PI = 3.14159

# ── main.py ──
# Import the entire module
import helpers

cleaned = helpers.clean_text("  Hello   World  ")
print(cleaned)  # "hello world"
print(helpers.PI)  # 3.14159

# Import specific items
from helpers import clean_text, validate_email

cleaned = clean_text("  Hello   World  ")
is_valid = validate_email("user@example.com")

# Import with alias
import helpers as h
cleaned = h.clean_text("Hello")

# Import everything (avoid in production — pollutes namespace)
from helpers import *

Where Python Looks for Modules

When you write import mymodule, Python searches these locations in order:

import sys
print(sys.path)
# [
#   '',                              # 1. Current directory
#   '/usr/lib/python3.12',           # 2. Standard library
#   '/usr/lib/python3.12/lib-dynload',
#   '/home/user/project/venv/lib/python3.12/site-packages',  # 3. venv packages
# ]

📖 Module Search Order

1. The directory containing the script being run (or current directory)

2. Directories in the PYTHONPATH environment variable

3. Standard library directories

4. Site-packages (where pip installs third-party packages)

This order explains why a local file named random.py would shadow Python's built-in random module — a common gotcha!

⚠️ Don't Shadow Standard Library Modules

Never name your files random.py, json.py, email.py, test.py, string.py, or any other standard library module name. Python will import your file instead of the built-in, causing confusing errors like AttributeError: module 'random' has no attribute 'randint'.

📂 Python Packages

A package is a directory containing Python modules and a special __init__.py file. Packages let you organize related modules into a hierarchy.

Basic Package Structure

# my_package/
# ├── __init__.py       ← Makes this directory a package
# ├── core.py           ← Module: my_package.core
# ├── utils.py          ← Module: my_package.utils
# └── models/           ← Sub-package
#     ├── __init__.py
#     ├── user.py        ← Module: my_package.models.user
#     └── product.py     ← Module: my_package.models.product

The __init__.py File

# ── my_package/__init__.py ──
"""My awesome package for data processing."""

# This file runs when someone imports the package.
# Use it to define the public API.

# Re-export commonly used items for convenient access
from .core import process_data, analyze
from .utils import clean_text

# Package-level constants
__version__ = "1.0.0"
__author__ = "Your Name"

# Now users can do:
# from my_package import process_data, clean_text
# instead of:
# from my_package.core import process_data

Importing from Packages

# Import the package (runs __init__.py)
import my_package
my_package.process_data(data)

# Import a specific module
from my_package import utils
utils.clean_text("hello")

# Import a specific function from a module
from my_package.models.user import User
user = User("Alice")

# Relative imports (from within the package)
# ── my_package/core.py ──
from .utils import clean_text        # Same level (.)
from .models.user import User         # Sub-package
from ..other_package import helper    # Parent level (..) — less common
graph TD A["import my_package.core"] --> B{"Is my_package
a directory?"} B -->|Yes| C["Run __init__.py"] C --> D{"Find core.py
in my_package/"} D -->|Found| E["Execute core.py"] E --> F["✅ my_package.core available"] B -->|No| G["❌ ModuleNotFoundError"] D -->|Not found| G style F fill:#10b981,color:#fff style G fill:#ef4444,color:#fff

🧠 __init__.py — Required or Optional?

In Python 3.3+, __init__.py is technically optional — directories without it become "namespace packages." However, for normal projects, always include it. It clearly marks the directory as a package, gives you a place to define the public API, and ensures predictable import behavior. An empty __init__.py is perfectly fine.

🏗️ Project Structure

A well-organized project makes your code easier to navigate, test, and share. Here's the standard layout that the Python community has converged on:

Small Script Project

# my_script/
# ├── main.py              ← Entry point
# ├── helpers.py            ← Utility functions
# ├── requirements.txt      ← Dependencies
# ├── .gitignore
# └── README.md

Application Project

# my_app/
# ├── src/
# │   └── my_app/           ← Main package
# │       ├── __init__.py
# │       ├── __main__.py    ← Enables: python -m my_app
# │       ├── cli.py         ← Command-line interface
# │       ├── core.py        ← Business logic
# │       ├── models.py      ← Data models
# │       ├── utils.py       ← Utilities
# │       └── config.py      ← Configuration
# ├── tests/
# │   ├── __init__.py
# │   ├── test_core.py
# │   ├── test_models.py
# │   └── test_utils.py
# ├── data/                  ← Sample data, fixtures
# ├── docs/                  ← Documentation
# ├── requirements.txt
# ├── dev-requirements.txt
# ├── pyproject.toml         ← Modern project metadata
# ├── .gitignore
# └── README.md

Key Files Explained

File Purpose
__init__.py Marks a directory as a Python package; defines the public API
__main__.py Entry point when running python -m my_app
requirements.txt Production dependencies with pinned versions
dev-requirements.txt Development tools (pytest, linters, formatters)
pyproject.toml Modern project metadata, build config, and tool settings
.gitignore Exclude venv/, __pycache__/, .pyc files from Git
README.md Project description, setup instructions, usage examples

A Practical .gitignore

# Virtual environments
venv/
.venv/
env/

# Python cache
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/

# IDE settings
.vscode/
.idea/
*.swp

# Environment variables
.env

# OS files
.DS_Store
Thumbs.db

🎯 if __name__ == "__main__"

This is one of Python's most important idioms. Every Python file has a built-in variable called __name__. Its value depends on how the file is used:

  • Run directly (python myfile.py): __name__ is "__main__"
  • Imported (import myfile): __name__ is "myfile"
# ── math_tools.py ──
"""A collection of math utility functions."""

def factorial(n):
    """Compute n! iteratively."""
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

def is_prime(n):
    """Check if n is a prime number."""
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True


# This block ONLY runs when the file is executed directly
if __name__ == "__main__":
    # Test the functions
    print(f"5! = {factorial(5)}")        # 120
    print(f"7 is prime: {is_prime(7)}")  # True
    print(f"10 is prime: {is_prime(10)}")# False

    # Run a demo
    primes = [n for n in range(2, 50) if is_prime(n)]
    print(f"Primes under 50: {primes}")
# ── main.py ──
# When imported, only the functions are available — the test code doesn't run
from math_tools import factorial, is_prime

print(factorial(10))   # 3628800
print(is_prime(97))    # True
# The demo/test block in math_tools.py did NOT execute!
$ python math_tools.py __name__ == "__main__" ✅ import math_tools __name__ == "math_tools" Functions defined + test code runs Prints demo output to console Functions defined only Test code is skipped silently

Common Patterns

# Pattern 1: Simple script with a main() function
def main():
    """Application entry point."""
    print("Starting application...")
    # ... your code here ...

if __name__ == "__main__":
    main()

# Pattern 2: CLI with argument parsing
import argparse

def main():
    parser = argparse.ArgumentParser(description="Process data files")
    parser.add_argument("input", help="Input file path")
    parser.add_argument("-o", "--output", default="output.csv", help="Output file")
    parser.add_argument("-v", "--verbose", action="store_true")
    args = parser.parse_args()

    # ... process args.input ...

if __name__ == "__main__":
    main()

# Pattern 3: __main__.py for package execution
# ── my_app/__main__.py ──
"""Allow running the package with: python -m my_app"""
from .cli import main
main()

✅ Why This Matters

The if __name__ == "__main__" guard makes your modules dual-purpose: they work as importable libraries AND as standalone scripts. Without it, any test/demo code at module level would run every time someone imports your module — a nasty surprise.

🚀 Beyond the Basics

The venv + pip + requirements.txt workflow covers most needs. Here's a quick tour of tools you'll encounter as projects grow.

pyproject.toml — The Modern Standard

pyproject.toml (PEP 621) is the modern way to declare project metadata. It replaces the older setup.py and setup.cfg files:

# pyproject.toml
[project]
name = "my-app"
version = "1.0.0"
description = "A useful application"
requires-python = ">=3.10"
dependencies = [
    "requests>=2.28",
    "flask~=2.3",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0",
    "black>=23.0",
]

[project.scripts]
my-app = "my_app.cli:main"

Alternative Tools

Tool What It Does When to Use
pipenv Combines venv + pip + lockfile Simpler workflow, auto-manages venvs
poetry Full project management + dependency resolution Libraries, complex dependency trees
uv Ultra-fast pip replacement (written in Rust) Drop-in replacement for pip, 10-100× faster
conda Package + environment manager for Python + non-Python Data science, C libraries, cross-language deps
pip-tools Generates pinned requirements from abstract deps Fine-grained control over dependency locking

🧠 Which Tool Should I Start With?

Start with venv + pip + requirements.txt — it's built-in, universally understood, and sufficient for most projects. Graduate to poetry or uv when you're building libraries or need deterministic lockfiles. Use conda if you're doing heavy data science with non-Python dependencies like CUDA or BLAS.

🏋️ Hands-on Exercises

🏋️ Exercise 1: Environment Setup

Objective: Practice the full venv + pip workflow from scratch.

Requirements:

  1. Create a new project directory called weather_app
  2. Create a virtual environment inside it
  3. Activate the virtual environment
  4. Install requests and python-dotenv
  5. Create a requirements.txt with pip freeze
  6. Create a .gitignore that excludes the venv and cache files
  7. Verify by deactivating, re-activating, and running pip list
💡 Hint

The commands in order: mkdir weather_app && cd weather_app, python -m venv venv, source venv/bin/activate (or venv\Scripts\activate on Windows), pip install requests python-dotenv, pip freeze > requirements.txt.

✅ Solution
# Terminal commands (Linux/macOS — adapt for Windows):

# 1. Create project directory
# mkdir weather_app
# cd weather_app

# 2. Create virtual environment
# python -m venv venv

# 3. Activate it
# source venv/bin/activate
# (venv) $   ← prompt should change

# 4. Install packages
# pip install requests python-dotenv

# 5. Freeze dependencies
# pip freeze > requirements.txt
# cat requirements.txt
# certifi==2023.7.22
# charset-normalizer==3.3.2
# idna==3.6
# python-dotenv==1.0.0
# requests==2.31.0
# urllib3==2.1.0

# 6. Create .gitignore
# Create a file named .gitignore with this content:
# venv/
# __pycache__/
# *.pyc
# .env

# 7. Verify
# deactivate
# source venv/bin/activate
# pip list
# Package            Version
# certifi            2023.7.22
# charset-normalizer 3.3.2
# idna               3.6
# pip                23.3.1
# python-dotenv      1.0.0
# requests           2.31.0
# urllib3             2.1.0

# ✅ All packages intact after reactivation!

🏋️ Exercise 2: Module & Package Organization

Objective: Create a properly structured Python package with modules, imports, and the __name__ guard.

Requirements:

  1. Create a package called textkit with three modules:
    • cleaners.py — functions: strip_html(text), normalize_whitespace(text)
    • analyzers.py — functions: word_count(text), char_frequency(text)
    • formatters.py — functions: title_case(text), truncate(text, max_len)
  2. Write an __init__.py that re-exports the most common functions
  3. Each module should have a if __name__ == "__main__" block with demo code
  4. Create a main.py outside the package that imports and uses the functions
💡 Hint

The directory structure should be: main.py at the top level, and textkit/ as a subdirectory with __init__.py, cleaners.py, analyzers.py, and formatters.py. Use relative imports in __init__.py: from .cleaners import strip_html.

✅ Solution
# ── textkit/__init__.py ──
"""textkit — A lightweight text processing toolkit."""

from .cleaners import strip_html, normalize_whitespace
from .analyzers import word_count, char_frequency
from .formatters import title_case, truncate

__version__ = "1.0.0"

# ── textkit/cleaners.py ──
"""Text cleaning utilities."""
import re

def strip_html(text):
    """Remove HTML tags from text."""
    return re.sub(r"<[^>]+>", "", text)

def normalize_whitespace(text):
    """Collapse multiple whitespace characters into single spaces."""
    return " ".join(text.split())

if __name__ == "__main__":
    html = "<h1>Hello</h1> <p>World</p>"
    print(f"strip_html: '{strip_html(html)}'")
    # strip_html: 'Hello World'

    messy = "  too   many    spaces   "
    print(f"normalize: '{normalize_whitespace(messy)}'")
    # normalize: 'too many spaces'

# ── textkit/analyzers.py ──
"""Text analysis utilities."""
from collections import Counter

def word_count(text):
    """Count the number of words in text."""
    return len(text.split())

def char_frequency(text):
    """Return a Counter of character frequencies (lowercase, no spaces)."""
    return Counter(text.lower().replace(" ", ""))

if __name__ == "__main__":
    sample = "Hello World Hello Python"
    print(f"Word count: {word_count(sample)}")
    # Word count: 4

    freq = char_frequency(sample)
    print(f"Top 5 chars: {freq.most_common(5)}")
    # Top 5 chars: [('l', 5), ('o', 3), ('h', 2), ('e', 1), ...]

# ── textkit/formatters.py ──
"""Text formatting utilities."""

def title_case(text):
    """Convert text to title case, handling common small words."""
    small_words = {"a", "an", "the", "and", "but", "or", "in", "on", "at", "to", "of"}
    words = text.lower().split()
    result = []
    for i, word in enumerate(words):
        if i == 0 or word not in small_words:
            result.append(word.capitalize())
        else:
            result.append(word)
    return " ".join(result)

def truncate(text, max_len=50):
    """Truncate text to max_len characters, adding ellipsis if needed."""
    if len(text) <= max_len:
        return text
    return text[:max_len - 3].rstrip() + "..."

if __name__ == "__main__":
    title = "the quick brown fox jumps over the lazy dog"
    print(f"Title case: '{title_case(title)}'")
    # Title case: 'The Quick Brown Fox Jumps Over the Lazy Dog'

    long_text = "This is a very long sentence that needs to be truncated"
    print(f"Truncated: '{truncate(long_text, 30)}'")
    # Truncated: 'This is a very long senten...'

# ── main.py ──
"""Demo of the textkit package."""
from textkit import strip_html, word_count, title_case, truncate

# Clean HTML content
html = "<h1>Breaking News</h1><p>Python 4.0 announced!</p>"
clean = strip_html(html)
print(f"Cleaned: {clean}")
# Cleaned: Breaking News Python 4.0 announced!

# Analyze
print(f"Words: {word_count(clean)}")
# Words: 5

# Format
print(f"Title: {title_case(clean)}")
# Title: Breaking News Python 4.0 Announced!

print(f"Short: {truncate(clean, 20)}")
# Short: Breaking News Py...

🎯 Quick Quiz

Question 1: Why should you use a virtual environment instead of installing packages into the system Python?

Question 2: What command creates a requirements.txt with exact versions of all installed packages?

Question 3: What is the value of __name__ when a Python file is imported as a module?

📏 Best Practices

✅ Do's

  • Always use a virtual environment — even for small scripts. It takes 5 seconds and prevents future headaches
  • Name your venv venv or .venv — it's a convention that tools and IDEs recognize automatically
  • Keep requirements.txt updated — run pip freeze > requirements.txt whenever you add or remove packages
  • Use if __name__ == "__main__" — in every module that has runnable code, even if "nobody will import it." You never know.
  • Include an empty __init__.py in every package directory — it's explicit and ensures correct import behavior
  • Add venv/ and __pycache__/ to .gitignore — these should never be in version control
  • Use python -m pip instead of bare pip — guarantees you're using the right Python's pip

❌ Don'ts

  • Don't install packages globally with sudo pip install — this pollutes the system Python and can break OS tools
  • Don't commit your venv/ directory — it's large, platform-specific, and unnecessary if you have requirements.txt
  • Don't name files after standard library modulesrandom.py, json.py, email.py will shadow the built-ins
  • Don't use from module import * in production — it pollutes the namespace and makes it hard to track where names come from
  • Don't put .env files with secrets in Git — add .env to .gitignore and use .env.example as a template

💡 Pro Tips

  • Use python -m venv --upgrade-deps venv to create a venv with the latest pip and setuptools pre-installed
  • VS Code and PyCharm auto-detect venv/ and .venv/ directories and activate them for you
  • Use pip install -e . (editable install) during development so you can import your package without reinstalling after every change
  • The pipdeptree package visualizes your dependency tree: pip install pipdeptree && pipdeptree
  • Consider uv as a drop-in pip replacement — same commands, 10-100× faster installation

📝 Summary

🎉 Key Takeaways

  • Virtual environments isolate project dependencies — python -m venv venv + activate
  • pip install adds packages to the active environment; pip freeze captures exact versions
  • requirements.txt is the standard for declaring and reproducing dependencies
  • Modules are .py files; packages are directories with __init__.py
  • Import system searches: current dir → PYTHONPATH → stdlib → site-packages
  • if __name__ == "__main__" makes modules work as both libraries and standalone scripts
  • Project structure: separate source, tests, docs; use .gitignore; never commit venvs
Command Purpose
python -m venv venv Create virtual environment
source venv/bin/activate Activate venv (Linux/macOS)
venv\Scripts\activate Activate venv (Windows)
deactivate Deactivate current venv
pip install package Install a package
pip install -r requirements.txt Install all listed dependencies
pip freeze > requirements.txt Save current environment
pip list Show installed packages
pip show package Package details & dependencies
pip list --outdated Check for updates

📚 Additional Resources

🚀 What's Next?

In the next lesson, we'll put your environment skills to work with Working with APIs — using the requests library to interact with REST APIs, parsing JSON responses, handling authentication, and building a data dashboard.

🎉 Module 3 Complete!

You've completed the Pythonic Code & Performance module! You can now write elegant comprehensions, build memory-efficient generators, leverage itertools/functools, and manage projects with virtual environments. You're fully equipped for real-world Python development.