This lesson on Project Structure & Packaging is hands-on and example-driven. You will isolate project dependencies using dedicated virtual environments, organize reusable Python code into modular packages with package initialization files, and build distributable wheel artifacts using pyproject.toml specifications. You can then install and import your custom packages locally across independent projects or prepare them for repository publication.
What You'll Be Able To Do
- Isolate project dependencies by creating and activating clean virtual environments using venv.
- Structure reusable code by creating modular Python files and package directory hierarchies.
- Expose package-level functions and attributes directly through init.py files.
- Configure project metadata, dependencies, and package discovery inside pyproject.toml.
- Build distributable wheel and source distribution archives using python -m build.
- Install local wheel builds across separate project environments using pip.
Detailed Concept Walkthrough
1. Virtual Environments and Dependency Isolation
A virtual environment is an isolated directory tree containing a dedicated Python interpreter and package set. It prevents dependency pollution across multiple projects by decoupling project-specific requirements from the global system Python installation.
- Mechanism: Creating a virtual environment clones the global Python interpreter binary into a standalone workspace without copying globally installed third-party libraries. This ensures that new installations remain strictly scoped to the active project.
- Under the Hood: When an environment is active, the workspace runtime directs pip to install packages into a local site-packages folder rather than the OS-wide directory, keeping global site-packages untouched.
- Best Practice: Always inspect the active environment using pip list before installing libraries to verify that you are not polluting the global operating system runtime.
# List packages in the current environment to verify clean state
pip list
# Create a standard virtual environment named .venv using the built-in venv module
python -m venv .venv
# Inspect the environment to confirm only basic tooling (e.g., pip) is installed
pip list
Key Takeaway: Always isolate project dependencies in dedicated virtual environments to guarantee predictable, reproducible execution across environments.
2. Modules versus Packages
A module is an individual Python file (.py) containing executable code, functions, or classes, whereas a package is a directory containing one or more modules. Packages organize modules into a structured namespace hierarchy.
- Mechanism: Any valid Python file in the working directory can be imported as a module using its filename prefix (e.g., importing simple from simple.py). A subdirectory acts as a package namespace, allowing nested modules to be accessed via dot notation (e.g., utils.add).
- Under the Hood: When resolving imports, Python searches the current working directory and paths listed in sys.path. Nested modules are dynamically loaded into sys.modules under their fully qualified hierarchical names.
- Syntax Rule: To import specific functions from nested modules within a package directory, use the syntax 'from package.module import function_name'.
# File: utils/add.py
def add_numbers(a, b):
"""Module-level function inside the utils package."""
return a + b
# File: main.py
# Import the function from the 'add' module inside the 'utils' package directory
from utils.add import add_numbers
result = add_numbers(3, 2)
print(result) # Outputs: 5
Key Takeaway: Modules are single Python source files, while packages are directory containers that group related modules into coherent namespaces.
3. Package Initialization with Dunder Init
The init.py (dunder init) file marks a directory as an importable package and allows the directory itself to behave like a top-level module. It provides an entry point for defining package-level symbols and orchestrating exposed APIs.
- Mechanism: Placing an init.py file inside a folder enables direct imports from the package root without requiring consumers to reference internal submodule filenames.
- Execution Flow: When a package is imported, Python automatically executes init.py first, initializing the package namespace and loading any functions, variables, or submodules defined within it.
- Best Practice: Use init.py to export a clean public API for your package so consumers do not need to know the internal directory layout or individual module names.
# File: utils/__init__.py
def sub_numbers(a, b):
"""Function exposed at the root package level."""
return a - b
# File: main.py
# Directly import from the package namespace without referencing __init__ or submodules
from utils import sub_numbers
result = sub_numbers(9, 1)
print(result) # Outputs: 8
Key Takeaway: The init.py file enables a directory to be imported directly as a module and exposes its public interface.
4. Packaging and Distribution with pyproject.toml
PEP 518 establishes pyproject.toml as the single source of truth for build configuration, project metadata, and dependencies. It supersedes legacy setup.py and requirements.txt workflows by providing a standardized build interface.
- Mechanism: The pyproject.toml configuration specifies the build backend (such as setuptools and wheel) along with project metadata, dependencies, and target package directories to include in distribution artifacts.
- Under the Hood: Executing the build front-end (python -m build) parses pyproject.toml, gathers source packages containing init.py, and generates both a wheel (.whl) binary distribution and a source archive (.tar.gz) in a dist/ directory.
- Best Practice: Separate the distributable package name defined in pyproject.toml from internal module directory names, and install the compiled .whl file in downstream projects using pip.
# File: pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
dependencies = []
[tool.setuptools.packages.find]
where = ["."]
include = ["utils*"]
Key Takeaway: Modern Python packaging standardizes build metadata in pyproject.toml to create portable wheel archives via python -m build.
Topics Covered in Project Structure & Packaging
- Project Setup and Environment Isolation (0:00 - 1:05) — Demonstrates why system Python should be isolated and configures a clean virtual environment using VS Code.
- Creating and Importing Modules (1:06 - 1:48) — Explains that any Python source file is a module by importing a function from simple.py into main.py.
- Defining Package Directories (1:49 - 2:40) — Introduces package directories as containers for modules and imports functions using dot-notation module paths.
- Exposing Functions with init.py (2:41 - 3:28) — Shows how adding a dunder init file allows functions to be imported directly from the package namespace.
- Configuring pyproject.toml and Build Tools (3:29 - 4:20) — Sets up PEP 518 package metadata and installs build dependencies to generate distribution artifacts.
- Building and Installing Local Wheels (4:21 - 5:30) — Executes python -m build to create wheel archives and installs the resulting artifact into a second independent project.
Python Cheat Sheet
-
pip list— Lists all installed packages in the active environmentpip list -
from <module> import <func>— Imports a specific function from a Python filefrom simple import say_hello -
from <pkg>.<mod> import <func>— Imports a function from a module inside a packagefrom utils.add import add_numbers -
__init__.py— Designates directory as package and defines root namespace# utils/__init__.py def sub_numbers(a, b): return a - b -
python -m build— Builds wheel and source distributions into dist/ folderpython -m build -
pip install <path_to_wheel>— Installs a pre-built wheel package from a local directorypip install ../P1/dist/my_package-0.1.0-py3-none-any.whl
Comparison Table
| Artifact / Concept | File Structure | Primary Purpose |
|---|---|---|
| Module | Single .py file | Stores reusable functions and classes |
| Package | Directory with modules & init.py | Groups related modules under namespace |
| Distribution Wheel (.whl) | Zipped archive from pyproject.toml | Distributes installable code across projects |
Common Pitfalls
- Mistake: Developing projects directly inside the global system Python environment. Avoid: Create and activate an isolated virtual environment before installing project-specific packages.
- Mistake: Forgetting to create init.py in package subdirectories before running build tools. Avoid: Ensure every package directory contains an init.py file so build tools discover and package it.
- Mistake: Relying on legacy setup.py scripts for modern project packaging configurations. Avoid: Define project metadata, build backend tools, and dependencies inside a standardized pyproject.toml file.
FAQs
- Why should I use a virtual environment instead of system Python? A virtual environment isolates dependencies per project, preventing version conflicts and eliminating unnecessary bloat in the system Python installation.
- Is init.py mandatory for importing submodules in Python 3? While Python 3 supports implicit namespace packages without it, init.py is required to expose root package functions and enable standard package discovery tools.
- What is the difference between a wheel (.whl) and a source distribution (.tar.gz)? A wheel is a pre-built, ready-to-install package format, whereas a source distribution is an unbuilt archive containing raw source files.
- Does pyproject.toml completely replace requirements.txt? Yes, pyproject.toml acts as the single source of truth for package dependencies and build metadata in modern packaging workflows under PEP 518.